From 604c2bdd270b57334114a7c40387c482f57f634c Mon Sep 17 00:00:00 2001 From: itsscb Date: Sun, 5 Nov 2023 22:41:38 +0100 Subject: [PATCH] ft/adds auto logout and persons_page --- .../lib/model/services/backend_service.dart | 65 ++++- .../app/lib/model/view_model/persons_vm.dart | 29 +++ frontend/app/lib/pages/home_page.dart | 244 +++++++++++------- frontend/app/lib/pages/login_overlay.dart | 48 +++- frontend/app/lib/pages/persons_page.dart | 174 +++++++++++++ frontend/app/lib/util/validation.dart | 27 ++ 6 files changed, 478 insertions(+), 109 deletions(-) create mode 100644 frontend/app/lib/model/view_model/persons_vm.dart create mode 100644 frontend/app/lib/pages/persons_page.dart create mode 100644 frontend/app/lib/util/validation.dart diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index f67b83d..669bc3e 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -5,9 +5,11 @@ import 'package:app/pb/account.pb.dart'; import 'package:app/pb/account_info.pb.dart'; import 'package:app/pb/person.pb.dart'; import 'package:app/data/database.dart'; +import 'package:app/pb/rpc_create_account.pb.dart'; import 'package:app/pb/rpc_get_account.pb.dart'; import 'package:app/pb/rpc_get_account_info.pb.dart'; import 'package:app/pb/rpc_get_person.pb.dart'; +import 'package:app/pb/rpc_list_persons.pb.dart'; import 'package:app/pb/rpc_login.pb.dart'; import 'package:app/pb/rpc_refresh_token.pb.dart'; import 'package:app/pb/service_df.pbgrpc.dart'; @@ -87,20 +89,48 @@ class BackendService { } if (session.accessTokenExpiresAt == null) { + await logout(); return false; } if (session.refreshTokenExpiresAt == null) { + await logout(); return false; } if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + await logout(); return false; } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + Session s = await BackendService.refreshToken(session); + if (s == session) { + return false; + } + } + return true; } + static Future createAccount( + {required String email, required String password}) async { + try { + await BackendService.client.createAccount(CreateAccountRequest( + email: email, + password: password, + )); + + return await login(email: email, password: password); + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + Future getAccount() async { Session? session = await _isLoggedIn(); if (session == null) { @@ -177,6 +207,34 @@ class BackendService { } } + Future> listPersons() async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final ListPersonsResponse response = await _client.listPersons( + ListPersonsRequest( + accountId: session.accountId, + ), + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.persons; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + // Future> listPersons() async { // if (_session.accessToken == null) { // refreshToken(); @@ -221,10 +279,11 @@ class BackendService { } } - Future refreshToken(Session session) async { + static Future refreshToken(Session session) async { try { - final RefreshTokenResponse response = await _client.refreshToken( - RefreshTokenRequest(refreshToken: session.refreshToken)); + final RefreshTokenResponse response = await BackendService.client + .refreshToken( + RefreshTokenRequest(refreshToken: session.refreshToken)); session.accessToken = response.accessToken; session.accessTokenExpiresAt = response.accessTokenExpiresAt; session = await Session.updateToken(session); diff --git a/frontend/app/lib/model/view_model/persons_vm.dart b/frontend/app/lib/model/view_model/persons_vm.dart new file mode 100644 index 0000000..40e4606 --- /dev/null +++ b/frontend/app/lib/model/view_model/persons_vm.dart @@ -0,0 +1,29 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/pb/account.pb.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:flutter/material.dart'; + +class PersonsViewModel with ChangeNotifier { + PersonsViewModel() { + listPersons(); + } + ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + + final BackendService _service = BackendService(); + + ApiResponse get response { + return _apiResponse; + } + + void listPersons() async { + _apiResponse = ApiResponse.loading('Bereite alles vor'); + try { + _apiResponse = + ApiResponse.completed(await _service.listPersons(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } +} diff --git a/frontend/app/lib/pages/home_page.dart b/frontend/app/lib/pages/home_page.dart index d34c4d6..c2eb137 100644 --- a/frontend/app/lib/pages/home_page.dart +++ b/frontend/app/lib/pages/home_page.dart @@ -1,8 +1,8 @@ -import 'dart:io'; - +import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; import 'package:app/model/view_model/account_vm.dart'; import 'package:app/pages/login_overlay.dart'; +import 'package:app/pages/persons_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_navigation.dart'; import 'package:app/widgets/bottom_navigation_item.dart'; @@ -28,6 +28,12 @@ class _HomePageState extends State { void _init() async { _setLoading(true); _loggedin = await BackendService.isLoggedIn; + // if (!_loggedin) { + // await BackendService.logout(); + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute(builder: (builder) => HomePage()), + // (route) => false); + // } _setLoading(false); } @@ -37,6 +43,17 @@ class _HomePageState extends State { }); } + void _checkResponse(ApiResponse response) async { + print('${response.message}'); + if (response.status == Status.ERROR && + response.message!.contains('unauthorized')) { + await BackendService.logout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (builder) => const HomePage()), + (route) => false); + } + } + bool _loading = true; bool _loggedin = false; @override @@ -45,114 +62,143 @@ class _HomePageState extends State { child: ChangeNotifierProvider( create: (context) => AccountViewModel(), child: Consumer( - builder: (context, value, child) => Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - // flexibleSpace: Image.asset( - // 'lib/assets/logo_300x200.png', - // // height: 400, - // ), - ), - drawer: SideDrawer( - children: [ - const Spacer( - flex: 3, - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.question_answer, - color: Colors.white, - label: 'About', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.privacy_tip, - color: Colors.white, - label: 'Datenschutz', - ), - SideDrawerItem( - onPressed: () {}, - icon: Icons.apartment, - color: Colors.white, - label: 'Impressum', - ), - const Spacer( - flex: 1, - ), - if (_loggedin || value.response.data != null) + builder: (context, value, child) { + _checkResponse(value.response); + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + // flexibleSpace: Image.asset( + // 'lib/assets/logo_300x200.png', + // // height: 400, + // ), + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), SideDrawerItem( onPressed: () {}, - icon: Icons.logout, + icon: Icons.question_answer, color: Colors.white, - label: 'Logout', + label: 'About', ), - ], - ), - bottomNavigationBar: BottomNavigation( - children: [ - if (!_loggedin) ...[ - BottomNavigationItem( + SideDrawerItem( onPressed: () {}, - icon: Icons.person_add_alt, + icon: Icons.privacy_tip, color: Colors.white, - label: 'Registrieren', + label: 'Datenschutz', ), - BottomNavigationItem( - onPressed: () async { - _loggedin = await showLogin(context); - }, - icon: Icons.login, - color: Colors.white, - label: 'Login', - ), - ] else - BottomNavigationItem( + SideDrawerItem( onPressed: () {}, - icon: Icons.person_search, + icon: Icons.apartment, color: Colors.white, - label: 'Personen', + label: 'Impressum', ), - BottomNavigationItem( - onPressed: () {}, - icon: Icons.dashboard, - color: Colors.white, - label: 'Dashboard', - ), - ...[] - ], - ), - body: Padding( - padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), - child: Center( - child: _loading - ? const CircularProgressIndicator( - color: Colors.grey, - ) - : Column( - children: [ - Image.asset( - 'lib/assets/logo_300x200.png', - ), - const SizedBox( - height: 40, - ), - Text( - 'Digitale Spuren auf Knopfdruck entfernen' - .toUpperCase(), - textAlign: TextAlign.center, - style: const TextStyle( - fontFamily: 'sans-serif', - fontSize: 24, - height: 1.6, - fontWeight: FontWeight.normal, - letterSpacing: 6, - ), - ), - ], - ), + const Spacer( + flex: 1, + ), + if (_loggedin && value.response.data != null) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => const HomePage()), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], ), - ), - ), + bottomNavigationBar: BottomNavigation( + children: [ + if (!_loggedin) ...[ + BottomNavigationItem( + onPressed: () async { + final bool res = + await showLogin(context, registration: true); + setState(() { + _loggedin = res; + }); + }, + icon: Icons.person_add_alt, + color: Colors.white, + label: 'Registrieren', + ), + BottomNavigationItem( + onPressed: () async { + final bool res = await showLogin(context); + setState(() { + _loggedin = res; + }); + }, + icon: Icons.login, + color: Colors.white, + label: 'Login', + ), + ] else + BottomNavigationItem( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (builder) => const PersonsPage())); + }, + icon: Icons.person_search, + color: Colors.white, + label: 'Personen', + ), + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + ...[] + ], + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), + child: Center( + child: _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : Column( + children: [ + Image.asset( + 'lib/assets/logo_300x200.png', + ), + const SizedBox( + height: 40, + ), + Text( + 'Digitale Spuren auf Knopfdruck entfernen' + .toUpperCase(), + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6, + ), + ), + ], + ), + ), + ), + ); + }, ), ), ); diff --git a/frontend/app/lib/pages/login_overlay.dart b/frontend/app/lib/pages/login_overlay.dart index 6d59622..927dc46 100644 --- a/frontend/app/lib/pages/login_overlay.dart +++ b/frontend/app/lib/pages/login_overlay.dart @@ -1,8 +1,10 @@ import 'package:app/model/services/backend_service.dart'; import 'package:app/widgets/background.dart'; import 'package:flutter/material.dart'; +import 'package:app/util/validation.dart'; -Future showLogin(BuildContext context) async { +Future showLogin(BuildContext context, + {bool registration = false}) async { final formKey = GlobalKey(); final mailController = TextEditingController(); final passwordController = TextEditingController(); @@ -26,6 +28,23 @@ Future showLogin(BuildContext context) async { } } + void register() { + if (formKey.currentState!.validate()) { + submitted = true; + BackendService.createAccount( + email: mailController.text, + password: passwordController.text, + ).then( + (r) { + if (r) { + loggedin = r; + Navigator.pop(context, true); + } + }, + ); + } + } + await showModalBottomSheet( useSafeArea: true, isScrollControlled: true, @@ -48,9 +67,9 @@ Future showLogin(BuildContext context) async { const SizedBox( height: 30, ), - const Text( - 'Login', - style: TextStyle( + Text( + registration ? 'Registrieren' : 'Login', + style: const TextStyle( fontFamily: 'sans-serif', fontSize: 24, height: 1.6, @@ -68,6 +87,12 @@ Future showLogin(BuildContext context) async { height: 40, ), TextFormField( + autofocus: true, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // emailRegExp, + // ), + // ], controller: mailController, decoration: const InputDecoration( fillColor: Color.fromARGB(30, 255, 255, 255), @@ -79,7 +104,7 @@ Future showLogin(BuildContext context) async { ), keyboardType: TextInputType.emailAddress, validator: (value) { - if (value == null || value.isEmpty) { + if (value == null || !value.isValidEmail) { return 'Bitte eine gültige E-Mail Adresse eingeben'; } return null; @@ -89,6 +114,11 @@ Future showLogin(BuildContext context) async { style: const TextStyle( color: Colors.white, ), + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // passwordRegExp, + // ), + // ], controller: passwordController, decoration: const InputDecoration( fillColor: Color.fromARGB(30, 255, 255, 255), @@ -101,7 +131,7 @@ Future showLogin(BuildContext context) async { keyboardType: TextInputType.visiblePassword, obscureText: true, validator: (value) { - if (value == null || value.isEmpty) { + if (value == null || !value.isValidPassword) { return 'Bitte geben Sie Ihr Passwort ein'; } return null; @@ -122,7 +152,11 @@ Future showLogin(BuildContext context) async { child: const Icon(Icons.arrow_back), ), ElevatedButton( - onPressed: !submitted ? login : null, + onPressed: !submitted + ? !registration + ? login + : register + : null, child: const Icon(Icons.login), ), ], diff --git a/frontend/app/lib/pages/persons_page.dart b/frontend/app/lib/pages/persons_page.dart new file mode 100644 index 0000000..746dfad --- /dev/null +++ b/frontend/app/lib/pages/persons_page.dart @@ -0,0 +1,174 @@ +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/services/backend_service.dart'; +import 'package:app/model/view_model/persons_vm.dart'; +import 'package:app/pages/home_page.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:app/widgets/side_drawer.dart'; +import 'package:app/widgets/side_drawer_item.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PersonsPage extends StatefulWidget { + const PersonsPage({super.key}); + + @override + State createState() => _PersonsPageState(); +} + +class _PersonsPageState extends State { + @override + void initState() { + super.initState(); + _init(); + } + + void _init() async { + _setLoading(true); + _loggedin = await BackendService.isLoggedIn; + _setLoading(false); + } + + void _setLoading(bool loading) { + setState(() { + _loading = loading; + }); + } + + void _checkResponse(ApiResponse response) { + if (response.status == Status.ERROR && + response.message!.contains('unauthenticated')) { + BackendService.logout(); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (builder) => const HomePage()), + (route) => false); + } + } + + bool _loading = true; + bool _loggedin = false; + + List _personsList(List persons) { + final List list = []; + for (var p in persons) { + list.add(Card( + color: Colors.black, + child: Text( + '${p.firstname} ${p.lastname}', + style: const TextStyle(color: Colors.white), + ), + )); + } + return list; + } + + @override + Widget build(BuildContext context) { + return Background( + child: ChangeNotifierProvider( + create: (context) => PersonsViewModel(), + child: Consumer( + builder: (context, value, child) { + _checkResponse(value.response); + return Scaffold( + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + floatingActionButton: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.add), + ), + appBar: AppBar( + automaticallyImplyLeading: false, + ), + drawer: SideDrawer( + children: [ + const Spacer( + flex: 3, + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.question_answer, + color: Colors.white, + label: 'About', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.privacy_tip, + color: Colors.white, + label: 'Datenschutz', + ), + SideDrawerItem( + onPressed: () {}, + icon: Icons.apartment, + color: Colors.white, + label: 'Impressum', + ), + const Spacer( + flex: 1, + ), + if (_loggedin || value.response.data != null) + SideDrawerItem( + onPressed: () async { + setState(() { + _loading = true; + }); + await BackendService.logout(); + // ignore: use_build_context_synchronously + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (builder) => const HomePage()), + (route) => false); + setState(() { + _loggedin = false; + _loading = false; + }); + }, + icon: Icons.logout, + color: Colors.white, + label: 'Logout', + ), + ], + ), + bottomNavigationBar: BottomNavigation( + children: [ + BottomNavigationItem( + onPressed: () {}, + icon: Icons.dashboard, + color: Colors.white, + label: 'Dashboard', + ), + BottomNavigationItem( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: Icons.home, + color: Colors.white, + label: 'Home', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: _loading + ? const CircularProgressIndicator( + color: Colors.grey, + ) + : value.response.status == Status.COMPLETED + ? value.response.data.length > 0 + ? ListView( + children: _personsList( + (value.response.data as List))) + : const Text('Noch keine Personen angelegt') + : const Text('Lade Daten...'), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/frontend/app/lib/util/validation.dart b/frontend/app/lib/util/validation.dart new file mode 100644 index 0000000..a7aa9e5 --- /dev/null +++ b/frontend/app/lib/util/validation.dart @@ -0,0 +1,27 @@ +final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); +final nameRegExp = + RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$"); +final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$"); +final passwordRegExp = RegExp(r'^.+$'); + +extension valString on String { + bool get isValidEmail { + return emailRegExp.hasMatch(this); + } + + bool get isValidName { + return nameRegExp.hasMatch(this); + } + + bool get isValidPassword { + return passwordRegExp.hasMatch(this); + } + + bool get isNotEmpty { + return this != trim(); + } + + bool get isValidPhone { + return phoneRegExp.hasMatch(this); + } +}