]> git.proxmox.com Git - flutter/pve_flutter_frontend.git/commitdiff
add field validation to login form
authorTim Marx <t.marx@proxmox.com>
Fri, 17 Apr 2020 10:53:07 +0000 (12:53 +0200)
committerTim Marx <t.marx@proxmox.com>
Fri, 17 Apr 2020 10:53:07 +0000 (12:53 +0200)
Signed-off-by: Tim Marx <t.marx@proxmox.com>
lib/bloc/pve_authentication_bloc.dart
lib/bloc/pve_login_bloc.dart
lib/events/pve_login_events.dart
lib/main.dart
lib/pages/login_page.dart
lib/widgets/pve_login_form.dart

index fb90c7a6aa10144e5595a8307e24bc6424c2553d..0d9dcb68bda562feaa818a679f5d0bda54e27c6b 100644 (file)
@@ -3,8 +3,8 @@ import 'dart:async';
 import 'package:pve_flutter_frontend/bloc/proxmox_base_bloc.dart';
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
 
-class PveAuthenticationBloc extends ProxmoxBaseBloc<PveAuthenticationEvent,PveAuthenticationState>{
-
+class PveAuthenticationBloc
+    extends ProxmoxBaseBloc<PveAuthenticationEvent, PveAuthenticationState> {
   @override
   PveAuthenticationState get initialState => Unauthenticated();
 
@@ -20,15 +20,13 @@ class PveAuthenticationBloc extends ProxmoxBaseBloc<PveAuthenticationEvent,PveAu
       yield Authenticated(event.apiClient);
     }
     if (event is LoggedOut) {
+      await storeTicket('');
       yield Unauthenticated();
     }
   }
-
-
 }
 
-abstract class PveAuthenticationEvent {
-}
+abstract class PveAuthenticationEvent {}
 
 class AppStarted extends PveAuthenticationEvent {
   @override
@@ -49,8 +47,7 @@ class LoggedOut extends PveAuthenticationEvent {
   String toString() => 'LoggedOut';
 }
 
-abstract class PveAuthenticationState{
-}
+abstract class PveAuthenticationState {}
 
 class Uninitialized extends PveAuthenticationState {
   @override
@@ -69,4 +66,4 @@ class Authenticated extends PveAuthenticationState {
 class Unauthenticated extends PveAuthenticationState {
   @override
   String toString() => 'Unauthenticated';
-}
\ No newline at end of file
+}
index 57abfba9d9943738c2793d3ae67423d985493dcd..c2933e5542ffe37f42c56db98d312e12f34eb89b 100644 (file)
@@ -1,19 +1,24 @@
 import 'dart:async';
 
+import 'package:meta/meta.dart';
 import 'package:pve_flutter_frontend/bloc/proxmox_base_bloc.dart';
 import 'package:pve_flutter_frontend/events/pve_login_events.dart';
-import 'package:pve_flutter_frontend/states/pve_login_states.dart';
-import 'package:pve_flutter_frontend/utils/validators.dart';
-import 'package:rxdart/rxdart.dart';
+import 'package:pve_flutter_frontend/states/pve_login_state.dart';
 
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
     as proxclient;
+import 'package:pve_flutter_frontend/utils/validators.dart';
 
 class PveLoginBloc extends ProxmoxBaseBloc<PveLoginEvent, PveLoginState> {
-  PveLoginState get initialState => PveLoginState.empty();
+  final PveLoginState init;
+
+  PveLoginBloc({@required this.init});
+  PveLoginState get initialState => init;
 
   @override
   Stream<PveLoginState> processEvents(PveLoginEvent event) async* {
+    yield latestState.rebuild((b) => b..errorMessage = "");
+
     if (event is UsernameChanged) {
       yield* _mapUsernameChangedToState(event.username);
     } else if (event is PasswordChanged) {
@@ -26,40 +31,71 @@ class PveLoginBloc extends ProxmoxBaseBloc<PveLoginEvent, PveLoginState> {
           password: event.password,
           hostname: event.origin);
     }
+
+    if (event is LoadOrigin) {
+      final origin = await proxclient.getPlatformAwareOrigin();
+      yield latestState.rebuild((b) => b
+        ..origin = origin ?? ''
+        ..isBlank = false);
+    }
   }
 
   Stream<PveLoginState> _mapUsernameChangedToState(String username) async* {
 //TODO implement username validator?
-    yield latestState.rebuild((b) => b
-      ..isUsernameValid = true
-      ..errorMessage = "");
   }
 
   Stream<PveLoginState> _mapPasswordChangedToState(String password) async* {
 //TODO implement password validator?
-    yield latestState.rebuild((b) => b
-      ..isPasswordValid = true
-      ..errorMessage = "");
   }
 
   Stream<PveLoginState> _mapOriginChangedToState(String origin) async* {
-    proxclient.storePlatformAwareOrigin(origin);
-//TODO implement origin validator?
-    yield latestState.rebuild((b) => b..errorMessage = "");
+    if (origin.isNotEmpty) {
+      try {
+        var uri = Uri.parse(origin);
+        print(uri.pathSegments.length);
+        if (uri.pathSegments.length < 3 && Validators.isValidDnsName(origin)) {
+          uri = uri.replace(host: origin);
+        }
+        if (!uri.hasPort) {
+          uri = uri.replace(port: 8006);
+        }
+        if (!uri.hasScheme) {
+          uri = uri.replace(scheme: 'https');
+        }
+        print(uri.origin);
+        yield latestState.rebuild((b) => b
+          ..originFieldError = ''
+          ..origin = uri.origin);
+        await proxclient.storePlatformAwareOrigin(uri.origin);
+      } on StateError catch (e) {
+        yield latestState.rebuild((b) => b..originFieldError = e.message);
+      } catch (e) {
+        yield latestState
+            .rebuild((b) => b..originFieldError = 'Please check input');
+      }
+    }
   }
 
   Stream<PveLoginState> _mapLoginWithCredentialsPressedToState(
       {String username, String password, String hostname}) async* {
-    yield PveLoginState.loading();
+    yield latestState.rebuild((b) => b..isLoading = true);
     try {
       final client = await proxclient.authenticate(username, password);
-      yield PveLoginState.success(apiClient: client);
+
+      yield latestState.rebuild((b) => b
+        ..apiClient = client
+        ..isLoading = false
+        ..isSuccess = true);
     } on proxclient.ProxmoxApiException catch (e) {
-      yield PveLoginState.failure(e.message);
+      yield latestState.rebuild((b) => b
+        ..errorMessage = e.message
+        ..isLoading = false);
     } catch (e, trace) {
+      yield latestState.rebuild((b) => b
+        ..errorMessage = e.toString()
+        ..isLoading = false);
       print(e);
       print(trace);
-      yield PveLoginState.failure(e.toString());
     }
   }
 }
index aa99c2592084c46f51e48828443be8bf5a39e611..c7b18ceee8f82c51e94b72a9b47ce9981f01e135 100644 (file)
@@ -34,10 +34,13 @@ class LoginWithCredentialsPressed extends PveLoginEvent {
   final String password;
   final String origin;
 
-  LoginWithCredentialsPressed({@required this.username, @required this.password, this.origin});
+  LoginWithCredentialsPressed(
+      {@required this.username, @required this.password, this.origin});
 
   @override
   String toString() {
     return 'LoginWithCredentialsPressed { email: $username, password: $password, hostname: $origin }';
   }
-}
\ No newline at end of file
+}
+
+class LoadOrigin extends PveLoginEvent {}
index bf801d8dbd9da3c56d1938160871548ef7d020aa..97829d88713c0f6786e6a25d86418d8b935b4cfe 100644 (file)
@@ -8,11 +8,13 @@ import 'package:pve_flutter_frontend/bloc/pve_node_overview_bloc.dart';
 import 'package:pve_flutter_frontend/bloc/pve_qemu_overview_bloc.dart';
 import 'package:pve_flutter_frontend/bloc/pve_resource_bloc.dart';
 import 'package:pve_flutter_frontend/bloc/pve_task_log_bloc.dart';
+import 'package:pve_flutter_frontend/events/pve_login_events.dart';
 import 'package:pve_flutter_frontend/pages/404_page.dart';
 import 'package:pve_flutter_frontend/pages/login_page.dart';
 import 'package:pve_flutter_frontend/pages/main_layout_slim.dart';
 import 'package:pve_flutter_frontend/pages/main_layout_wide.dart';
 import 'package:pve_flutter_frontend/states/pve_cluster_status_state.dart';
+import 'package:pve_flutter_frontend/states/pve_login_state.dart';
 import 'package:pve_flutter_frontend/states/pve_lxc_overview_state.dart';
 import 'package:pve_flutter_frontend/states/pve_node_overview_state.dart';
 import 'package:pve_flutter_frontend/states/pve_qemu_overview_state.dart';
@@ -93,12 +95,23 @@ class MyApp extends StatelessWidget {
           scaffoldBackgroundColor: Colors.white,
         ),
         onGenerateRoute: (context) {
-          if (authbloc.state.value is Unauthenticated) {
+          if (authbloc.state.value is Unauthenticated ||
+              context.name == '/login') {
             return MaterialPageRoute(
               builder: (context) {
-                return PveLoginPage(
-                  loginBloc: PveLoginBloc(),
-                  authenticationBloc: authbloc,
+                return MultiProvider(
+                  providers: [
+                    Provider<PveLoginBloc>(
+                      create: (context) =>
+                          PveLoginBloc(init: PveLoginState.init(''))
+                            ..events.add(LoadOrigin()),
+                      dispose: (context, bloc) => bloc.dispose(),
+                    ),
+                    Provider.value(
+                      value: authbloc,
+                    )
+                  ],
+                  child: PveLoginPage(),
                 );
               },
             );
@@ -210,6 +223,31 @@ class MyApp extends StatelessWidget {
               );
             }
             switch (context.name) {
+              case '/':
+                return MaterialPageRoute(
+                  fullscreenDialog: true,
+                  settings: context,
+                  builder: (context) => MultiProvider(
+                    providers: [
+                      Provider<proxclient.ProxmoxApiClient>.value(
+                        value: state.apiClient,
+                      ),
+                      Provider<PveClusterStatusBloc>(
+                        create: (context) => PveClusterStatusBloc(
+                            apiClient: state.apiClient,
+                            init: PveClusterStatusState.init())
+                          ..events.add(UpdateClusterStatus()),
+                        dispose: (context, bloc) => bloc.dispose(),
+                      )
+                    ],
+                    child: ProxmoxLayoutBuilder(
+                      builder: (context, layout) => layout != ProxmoxLayout.slim
+                          ? MainLayoutWide()
+                          : MainLayoutSlim(),
+                    ),
+                  ),
+                );
+                break;
               case PveCreateVmWizard.routeName:
                 return MaterialPageRoute(
                   fullscreenDialog: true,
@@ -219,6 +257,8 @@ class MyApp extends StatelessWidget {
                         value: state.apiClient, child: PveCreateVmWizard());
                   },
                 );
+                break;
+
               case PveConsoleWidget.routeName:
                 return MaterialPageRoute(
                   fullscreenDialog: true,
@@ -231,6 +271,8 @@ class MyApp extends StatelessWidget {
                         ));
                   },
                 );
+                break;
+
               default:
                 return MaterialPageRoute(
                   settings: context,
@@ -241,55 +283,8 @@ class MyApp extends StatelessWidget {
             }
           }
         },
-        home: RootPage(),
+        initialRoute: '/',
       ),
     );
   }
 }
-
-class RootPage extends StatelessWidget {
-  @override
-  Widget build(BuildContext context) {
-    final authBloc = Provider.of<PveAuthenticationBloc>(context);
-    return StreamBuilder<PveAuthenticationState>(
-        stream: authBloc.state,
-        builder: (context, snapshot) {
-          if (snapshot.hasData) {
-            final state = snapshot.data;
-
-            if (state is Unauthenticated) {
-              return PveLoginPage(
-                loginBloc: PveLoginBloc(),
-                authenticationBloc: authBloc,
-              );
-            }
-            if (state is Authenticated) {
-              return MultiProvider(
-                providers: [
-                  Provider<proxclient.ProxmoxApiClient>.value(
-                    value: state.apiClient,
-                  ),
-                  Provider<PveClusterStatusBloc>(
-                    create: (context) => PveClusterStatusBloc(
-                        apiClient: state.apiClient,
-                        init: PveClusterStatusState.init())
-                      ..events.add(UpdateClusterStatus()),
-                    dispose: (context, bloc) => bloc.dispose(),
-                  )
-                ],
-                child: ProxmoxLayoutBuilder(
-                  builder: (context, layout) => layout != ProxmoxLayout.slim
-                      ? MainLayoutWide()
-                      : MainLayoutSlim(),
-                ),
-              );
-            }
-
-            if (state is Uninitialized) {
-              return Container();
-            }
-          }
-          return Container();
-        });
-  }
-}
index 73c13c815c59a246a2870af6c7a2564c987b8d2d..0d6459a0833f8634a6842a1e99cd15aae4172914 100644 (file)
@@ -1,37 +1,16 @@
 import 'package:flutter/material.dart';
-import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart';
+import 'package:provider/provider.dart';
 import 'package:pve_flutter_frontend/bloc/pve_login_bloc.dart';
-import 'package:pve_flutter_frontend/widgets/pve_login_form.dart';
-
-class PveLoginPage extends StatefulWidget {
-  final PveLoginBloc loginBloc;
-  final PveAuthenticationBloc authenticationBloc;
-  static const routeName = '/login';
-
-  PveLoginPage({
-    Key key,
-    @required this.loginBloc,
-    @required this.authenticationBloc,
-  }) : super(key: key);
-
-  @override
-  State<PveLoginPage> createState() => _PveLoginPageState();
-}
-
-class _PveLoginPageState extends State<PveLoginPage> {
-  PveLoginBloc get _loginBloc => widget.loginBloc;
-  PveAuthenticationBloc get _authenticationBloc => widget.authenticationBloc;
+import 'package:pve_flutter_frontend/states/pve_login_state.dart';
+import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
 
-  @override
-  void initState() {
-    super.initState();
-    _loginBloc.state.where((state) => state.isSuccess).forEach(
-        (loginSucceded) => _authenticationBloc.events.add(
-            LoggedIn(loginSucceded.apiClient)));
-  }
+import 'package:pve_flutter_frontend/widgets/pve_login_form.dart';
 
+class PveLoginPage extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
+    final lBloc = Provider.of<PveLoginBloc>(context);
+
     return Container(
       decoration: BoxDecoration(
         gradient: LinearGradient(
@@ -47,8 +26,18 @@ class _PveLoginPageState extends State<PveLoginPage> {
           child: Center(
             child: ConstrainedBox(
               constraints: BoxConstraints(maxWidth: 400),
-              child: PveLoginForm(
-                loginBloc: _loginBloc,
+              child: ProxmoxStreamBuilder<PveLoginBloc, PveLoginState>(
+                bloc: lBloc,
+                builder: (context, state) {
+                  if (state.isBlank) {
+                    return Center(
+                      child: CircularProgressIndicator(),
+                    );
+                  }
+                  return PveLoginForm(
+                    savedOrigin: state.origin,
+                  );
+                },
               ),
             ),
           ),
@@ -56,10 +45,4 @@ class _PveLoginPageState extends State<PveLoginPage> {
       ),
     );
   }
-
-  @override
-  void dispose() {
-    _loginBloc.dispose();
-    super.dispose();
-  }
 }
index 3be5a535f3f5964ac074c5cd50c4d4dded152a6e..d03d486987adf01fcc76e3a4da106e05a4661e96 100644 (file)
@@ -2,14 +2,17 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:pve_flutter_frontend/bloc/pve_login_bloc.dart';
 import 'package:pve_flutter_frontend/events/pve_login_events.dart';
-import 'package:pve_flutter_frontend/states/pve_login_states.dart';
+import 'package:pve_flutter_frontend/states/pve_login_state.dart';
+import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
+import 'package:pve_flutter_frontend/widgets/proxmox_stream_listener.dart';
+import 'package:provider/provider.dart';
+import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart';
 
 class PveLoginForm extends StatefulWidget {
-  final PveLoginBloc loginBloc;
-
+  final String savedOrigin;
   PveLoginForm({
     Key key,
-    @required this.loginBloc,
+    this.savedOrigin,
   }) : super(key: key);
 
   @override
@@ -20,104 +23,100 @@ class _PveLoginFormState extends State<PveLoginForm> {
   final _originController = TextEditingController();
   final _usernameController = TextEditingController();
   final _passwordController = TextEditingController();
-
-  PveLoginBloc get _loginBloc => widget.loginBloc;
-
+  PveLoginBloc lBloc;
   @override
   void initState() {
     super.initState();
+    lBloc = Provider.of<PveLoginBloc>(context, listen: false);
     _passwordController.addListener(_onPasswordChanged);
     _usernameController.addListener(_onUsernameChanged);
     _originController.addListener(_onOriginChanged);
-  }
-
-  @override
-  void didChangeDependencies() {
-    super.didChangeDependencies();
-    _loginBloc.state.where((state) => state.isFailure).listen(
-          (state) => Scaffold.of(context).showSnackBar(
-            SnackBar(
-              content: Text(
-                state.errorMessage ?? "Error",
-                style: ThemeData.dark().textTheme.button,
-              ),
-              backgroundColor: ThemeData.dark().errorColor,
-              behavior: SnackBarBehavior.floating,
-            ),
-          ),
-        );
+    _originController.text = widget.savedOrigin;
   }
 
   @override
   Widget build(BuildContext context) {
-    return StreamBuilder<PveLoginState>(
-      stream: _loginBloc.state,
-      initialData: PveLoginState.empty(),
-      builder: (BuildContext context, AsyncSnapshot<PveLoginState> snapshot) {
-        if (snapshot.hasData) {
-          final state = snapshot.data;
-          return Theme(
-            data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
-            child: Form(
-              child: Column(
-                crossAxisAlignment: CrossAxisAlignment.center,
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: [
-                  Image.asset(
-                      'assets/images/Proxmox_logo_white_orange_800.png'),
-                  SizedBox(height: 20),
-                  //TODO change this when there's a more official way to determine web e.g. Platform.isWeb or similar
-                  if (!kIsWeb)
+    final aBloc = Provider.of<PveAuthenticationBloc>(context);
+    return StreamListener(
+      stream: lBloc.state.where((state) => state.isSuccess),
+      onStateChange: (newState) {
+        aBloc.events.add(LoggedIn(newState.apiClient));
+      },
+      child: StreamListener(
+        stream: aBloc.state.where((state) => state is Authenticated),
+        onStateChange: (newState) => Navigator.of(context).pushNamed('/'),
+        child: ProxmoxStreamBuilder<PveLoginBloc, PveLoginState>(
+          errorHandler: false,
+          bloc: lBloc,
+          builder: (context, state) {
+            return Theme(
+              data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
+              child: Form(
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    Image.asset(
+                        'assets/images/Proxmox_logo_white_orange_800.png'),
+                    SizedBox(height: 20),
+                    //TODO change this when there's a more official way to determine web e.g. Platform.isWeb or similar
+                    if (!kIsWeb)
+                      TextFormField(
+                        decoration: InputDecoration(
+                            icon: Icon(Icons.domain),
+                            labelText: 'Origin',
+                            hintText: 'e.g. https://ip-of-your-pve-host:8006'),
+                        controller: _originController,
+                        autovalidate: true,
+                        validator: (v) =>
+                            state.isOriginValid ? null : state.originFieldError,
+                      ),
+                    TextFormField(
+                      decoration: InputDecoration(
+                        icon: Icon(Icons.person),
+                        labelText: 'Username',
+                      ),
+                      controller: _usernameController,
+                      autovalidate: true,
+                      validator: (v) => state.isUsernameValid
+                          ? null
+                          : state.userNameFieldError,
+                    ),
                     TextFormField(
                       decoration: InputDecoration(
-                          icon: Icon(Icons.domain),
-                          labelText: 'Origin',
-                          hintText: 'e.g. https://ip-of-your-pve-host:8006'),
-                      controller: _originController,
+                        icon: Icon(Icons.lock),
+                        labelText: 'Password',
+                      ),
+                      controller: _passwordController,
+                      obscureText: true,
+                      autovalidate: true,
+                      autocorrect: false,
+                      validator: (v) => state.isPasswordValid
+                          ? null
+                          : state.passwordFieldError,
+                      onFieldSubmitted: (text) => isLoginButtonEnabled(state)
+                          ? _onLoginButtonPressed()
+                          : null,
                     ),
-                  TextFormField(
-                    decoration: InputDecoration(
-                      icon: Icon(Icons.person),
-                      labelText: 'Username',
+                    SizedBox(height: 20),
+                    RaisedButton(
+                      onPressed: isLoginButtonEnabled(state)
+                          ? _onLoginButtonPressed
+                          : null,
+                      color: Color(0xFFE47225),
+                      child: Text('Login'),
                     ),
-                    controller: _usernameController,
-                  ),
-                  TextFormField(
-                    decoration: InputDecoration(
-                      icon: Icon(Icons.lock),
-                      labelText: 'Password',
+                    Container(
+                      child:
+                          state.isLoading ? CircularProgressIndicator() : null,
                     ),
-                    controller: _passwordController,
-                    obscureText: true,
-                    autovalidate: true,
-                    autocorrect: false,
-                    validator: (_) {
-                      return !state.isPasswordValid ? 'Invalid Password' : null;
-                    },
-                    onFieldSubmitted: (text) => isLoginButtonEnabled(state)
-                        ? _onLoginButtonPressed()
-                        : null,
-                  ),
-                  SizedBox(height: 20),
-                  RaisedButton(
-                    onPressed: isLoginButtonEnabled(state)
-                        ? _onLoginButtonPressed
-                        : null,
-                    color: Color(0xFFE47225),
-                    child: Text('Login'),
-                  ),
-                  Container(
-                    child:
-                        state.isSubmitting ? CircularProgressIndicator() : null,
-                  ),
-                ],
+                  ],
+                ),
               ),
-            ),
-          );
-        } else {
-          return Container();
-        }
-      },
+            );
+          },
+        ),
+      ),
     );
   }
 
@@ -126,7 +125,7 @@ class _PveLoginFormState extends State<PveLoginForm> {
       _passwordController.text.isNotEmpty;
 
   bool isLoginButtonEnabled(PveLoginState state) {
-    return state.isFormValid && isPopulated && !state.isSubmitting;
+    return state.isFormValid && isPopulated && !state.isLoading;
   }
 
   void _onUsernameChanged() {
@@ -136,19 +135,19 @@ class _PveLoginFormState extends State<PveLoginForm> {
   }
 
   void _onPasswordChanged() {
-    _loginBloc.events.add(
+    lBloc.events.add(
       PasswordChanged(password: _passwordController.text),
     );
   }
 
   void _onOriginChanged() {
-    _loginBloc.events.add(
+    lBloc.events.add(
       OriginChanged(origin: _originController.text),
     );
   }
 
   _onLoginButtonPressed() {
-    _loginBloc.events.add(LoginWithCredentialsPressed(
+    lBloc.events.add(LoginWithCredentialsPressed(
         username: _usernameController.text.trim() + "@pam",
         password: _passwordController.text,
         origin: _originController.text.trim()));