diff --git a/frontend/app/lib/data/database.dart b/frontend/app/lib/data/database.dart index 9cf0335..7d7e411 100644 --- a/frontend/app/lib/data/database.dart +++ b/frontend/app/lib/data/database.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:app/pb/session.pbjson.dart'; import 'package:fixnum/fixnum.dart'; import 'package:app/pb/google/protobuf/timestamp.pb.dart'; @@ -40,6 +41,12 @@ class Session { ); Session s = Session(); s._database = db; + final sessions = await s.getSessions(); + if (sessions.isNotEmpty) { + final session = sessions[0]; + session._database = db; + return session; + } return s; } @@ -90,6 +97,41 @@ class Session { return 'Session{accountId: $accountId, sessionId: $sessionId, accessToken: $accessToken, accessTokenExpiresAt: ${accessTokenExpiresAt.toString()}, refreshToken: $refreshToken, refreshTokenExpiresAt: ${refreshTokenExpiresAt.toString()}}'; } + static newSession(Session session) async { + final db = await openDatabase( + join(await getDatabasesPath(), 'df_database.db'), + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE sessions(accountId INTEGER PRIMARY KEY, sessionId TEXT, accessToken TEXT, accessTokenExpiresAt TEXT, refreshToken TEXT, refreshTokenExpiresAt TEXT)', + ); + }, + version: 1, + ); + + await db.insert( + 'sessions', + session.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future updateToken(Session s) async { + final db = await openDatabase( + join(await getDatabasesPath(), 'df_database.db'), + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE sessions(accountId INTEGER PRIMARY KEY, sessionId TEXT, accessToken TEXT, accessTokenExpiresAt TEXT, refreshToken TEXT, refreshTokenExpiresAt TEXT)', + ); + }, + version: 1, + ); + await db.update( + 'sessions', + s.toMap(), + ); + return s; //await getSession(s.accountId!); + } + Future insertSession(Session session) async { // print('INSERTING SESSION: ${session.sessionId}'); final db = _database; @@ -101,6 +143,17 @@ class Session { // print('INSERT RESULT: $result'); } + Future updateSession(Session session) async { + sessionId = session.sessionId; + accessToken = session.accessToken; + accessTokenExpiresAt = session.accessTokenExpiresAt; + refreshToken = session.refreshToken; + refreshTokenExpiresAt = session.refreshTokenExpiresAt; + final db = _database; + await db.update('sessions', session.toMap()); + return session; + } + Future removeSession(String sessionId) async { final db = _database; await db.delete('sessions', where: 'sessionId = ?', whereArgs: [sessionId]); diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index 8e44a71..e790121 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -45,8 +45,8 @@ void main() async { )), home: Background( child: StartPage( - client: await GClient.client, - ), + // client: await GClient.client, + ), ), ), ); diff --git a/frontend/app/lib/model/apis/api_response.dart b/frontend/app/lib/model/apis/api_response.dart new file mode 100644 index 0000000..764c500 --- /dev/null +++ b/frontend/app/lib/model/apis/api_response.dart @@ -0,0 +1,17 @@ +class ApiResponse { + Status status; + T? data; + String? message; + + ApiResponse.initial(this.message) : status = Status.INITIAL; + ApiResponse.loading(this.message) : status = Status.LOADING; + ApiResponse.completed(this.data, this.message) : status = Status.COMPLETED; + ApiResponse.error(this.message) : status = Status.ERROR; + + @override + String toString() { + return "Status : $status \n Message: $message \n Data : $data"; + } +} + +enum Status { INITIAL, LOADING, COMPLETED, ERROR } diff --git a/frontend/app/lib/model/apis/app_exception.dart b/frontend/app/lib/model/apis/app_exception.dart new file mode 100644 index 0000000..7093126 --- /dev/null +++ b/frontend/app/lib/model/apis/app_exception.dart @@ -0,0 +1,32 @@ +class AppException implements Exception { + final _message; + final _prefix; + + AppException([this._message, this._prefix]); + + String toString() { + return "$_prefix$_message"; + } +} + +class FetchDataException extends AppException { + FetchDataException([String? message]) + : super(message, 'Fehler bei der Kommunikation: '); +} + +class BadRequestException extends AppException { + BadRequestException([message]) : super(message, 'Ungültige Anfrage; '); +} + +class UnauthorizedException extends AppException { + UnauthorizedException([message]) + : super(message, 'Nicht authorisierte Anfrage; '); +} + +class InvalidInputException extends AppException { + InvalidInputException([message]) : super(message, 'Ungültige Eingabe; '); +} + +class InternalException extends AppException { + InternalException([message]) : super(message, 'Interner Fehler; '); +} diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart new file mode 100644 index 0000000..6cc42d0 --- /dev/null +++ b/frontend/app/lib/model/services/backend_service.dart @@ -0,0 +1,237 @@ +import 'dart:io'; + +import 'package:app/model/apis/app_exception.dart'; +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_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_login.pb.dart'; +import 'package:app/pb/rpc_refresh_token.pb.dart'; +import 'package:app/pb/service_df.pbgrpc.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:grpc/grpc.dart'; +import 'package:sqflite/sqflite.dart'; + +class BackendService { + BackendService() { + _init(); + } + final String baseUrl = '10.0.0.2'; + final String port = '9090'; + + late Session _session; + + final dfClient _client = dfClient( + ClientChannel('10.0.2.2', + port: 9090, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + )), + options: CallOptions( + timeout: const Duration(seconds: 5), + ), + ); + + static get client => dfClient( + ClientChannel('10.0.2.2', + port: 9090, + options: const ChannelOptions( + credentials: ChannelCredentials.insecure(), + )), + options: CallOptions( + timeout: const Duration(seconds: 5), + ), + ); + + void _init() { + Session.session.then((value) => _session = value); + } + + Future _isLoggedIn() async { + Session session = await Session.session; + if (session.accessToken == null || + session.refreshToken == null || + session.accountId == null) { + return null; + } + + if (session.accessTokenExpiresAt == null) { + return null; + } + + if (session.refreshTokenExpiresAt == null) { + return null; + } + + if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + return null; + } else { + if (session.refreshToken != null && + session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + } + } + + return session; + } + + static Future get isLoggedIn async { + Session session = await Session.session; + if (session.accessToken == null || + session.refreshToken == null || + session.accountId == null) { + return false; + } + + if (session.accessTokenExpiresAt == null) { + return false; + } + + if (session.refreshTokenExpiresAt == null) { + return false; + } + + if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + return false; + } + + return true; + } + + Future getAccount() async { + Session? session = await _isLoggedIn(); + if (session == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + try { + final GetAccountResponse response = await _client.getAccount( + GetAccountRequest(id: session!.accountId), + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.account; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + // if (err.code == 16) { + // await refreshToken(session); + // return getAccount(); + // } + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future getAccountInfo() 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!.toDateTime().isBefore(DateTime.now())) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final GetAccountInfoResponse response = await _client + .getAccountInfo(GetAccountInfoRequest(accountId: _session.accountId)); + return response.accountInfo; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + if (err.code == 16) { + await refreshToken(session); + return getAccountInfo(); + } + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future getPerson(Int64 personId) 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 GetPersonResponse response = + await _client.getPerson(GetPersonRequest(id: personId)); + return response.person; + } 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(); + // } + // try { + // ListPersonsResponse response = + // await _client.listPersons(ListPersonsRequest(accountId: _session.accountId)); + // return response.persons; + // } on SocketException { + // throw FetchDataException('Keine Internet Verbindung'); + // } on GrpcError catch (err) { + // throw FetchDataException(err.message); + // } catch (err) { + // throw InternalException(err.toString()); + // } + // } + static Future login( + {required String email, required String password}) async { + try { + final LoginResponse response = await BackendService.client.login( + LoginRequest( + email: email, + password: password, + ), + ); + await Session.newSession(response as Session); + return response.accessToken != ''; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future refreshToken(Session session) async { + try { + final RefreshTokenResponse response = await _client.refreshToken( + RefreshTokenRequest(refreshToken: session.refreshToken)); + session.accessToken = response.accessToken; + session.accessTokenExpiresAt = response.accessTokenExpiresAt; + session = await Session.updateToken(session); + return session; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + static Future logout() async { + Session session = await Session.session; + session.reset(); + } +} diff --git a/frontend/app/lib/model/view_model/account_vm.dart b/frontend/app/lib/model/view_model/account_vm.dart new file mode 100644 index 0000000..13356fa --- /dev/null +++ b/frontend/app/lib/model/view_model/account_vm.dart @@ -0,0 +1,56 @@ +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:flutter/material.dart'; + +class AccountViewModel with ChangeNotifier { + AccountViewModel() { + _init(); + } + ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + + final BackendService _service = BackendService(); + Account? _account; + + ApiResponse get response { + return _apiResponse; + } + + Account? get account { + return _account; + } + + void _init() async { + _apiResponse = ApiResponse.loading('Bereite alles vor'); + try { + _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } + + Future fetchAccount() async { + _apiResponse = ApiResponse.loading('Hole Account'); + notifyListeners(); + try { + _apiResponse = ApiResponse.completed(await _service.getAccount(), 'done'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + notifyListeners(); + } + + Future logout() async { + _apiResponse = ApiResponse.loading('Logge aus'); + notifyListeners(); + try { + await BackendService.logout(); + _apiResponse = ApiResponse.completed(null, 'Erfolgreich ausgeloggt'); + } catch (e) { + _apiResponse = ApiResponse.error(e.toString()); + } + print(_apiResponse.message); + notifyListeners(); + } +} diff --git a/frontend/app/lib/pages/dashboard_page.dart b/frontend/app/lib/pages/dashboard_page.dart index 6be86ba..14d3a25 100644 --- a/frontend/app/lib/pages/dashboard_page.dart +++ b/frontend/app/lib/pages/dashboard_page.dart @@ -11,10 +11,10 @@ import 'package:flutter/material.dart'; class DashboardPage extends StatefulWidget { const DashboardPage({ super.key, - required this.client, + // required this.client, }); - final GClient client; + // final GClient client; @override State createState() => _DashboardPageState(); @@ -35,44 +35,44 @@ class _DashboardPageState extends State { super.initState(); _setLoading(true); - widget.client.getAccountInfo( - GetAccountInfoRequest( - accountId: widget.client.session.accountId, - ), - onError: ({String? msg}) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('AccountInfo konnte nicht geladen werden'), - action: msg != null - ? SnackBarAction( - label: 'Details', - textColor: Colors.grey, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: Text( - msg, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - icon: const Icon( - Icons.warning, - color: Colors.red, - ), - ); - }, - ); - }) - : null, - ), - ); - }, - ).then((value) { - accountInfo = value.accountInfo; - _setLoading(false); - }); + // widget.client.getAccountInfo( + // GetAccountInfoRequest( + // accountId: widget.client.session.accountId, + // ), + // onError: ({String? msg}) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: const Text('AccountInfo konnte nicht geladen werden'), + // action: msg != null + // ? SnackBarAction( + // label: 'Details', + // textColor: Colors.grey, + // onPressed: () { + // showDialog( + // context: context, + // builder: (context) { + // return AlertDialog( + // content: Text( + // msg, + // textAlign: TextAlign.center, + // style: const TextStyle(color: Colors.black), + // ), + // icon: const Icon( + // Icons.warning, + // color: Colors.red, + // ), + // ); + // }, + // ); + // }) + // : null, + // ), + // ); + // }, + // ).then((value) { + // accountInfo = value.accountInfo; + // _setLoading(false); + // }); } @override @@ -144,139 +144,139 @@ class _DashboardPageState extends State { ], ), ), - if (widget.client.session.accessToken != null) - TextButton( - onPressed: () { - widget.client.session.accessToken = null; - widget.client.session - .removeSession(widget.client.session.sessionId!); + // if (widget.client.session.accessToken != null) + // TextButton( + // onPressed: () { + // widget.client.session.accessToken = null; + // widget.client.session + // .removeSession(widget.client.session.sessionId!); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (context) => - StartPage(client: widget.client), - ), - (route) => false); - }, - child: const Row( - children: [ - Text( - 'Log out', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.logout, - color: Colors.white, - ), - ], - ), - ), + // Navigator.of(context).pushAndRemoveUntil( + // MaterialPageRoute( + // builder: (context) => + // StartPage(client: widget.client), + // ), + // (route) => false); + // }, + // child: const Row( + // children: [ + // Text( + // 'Log out', + // style: TextStyle(fontSize: 20), + // ), + // Spacer(), + // Icon( + // Icons.logout, + // color: Colors.white, + // ), + // ], + // ), + // ), const SizedBox( height: 250, ) ], ); }), - bottomNavigationBar: Builder( - builder: (context) { - return BottomBar( - children: widget.client.session.accessToken != null - ? [ - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Personen', - icon: Column( - children: [ - IconButton( - onPressed: () => - Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.group, - color: Colors.white, - ), - ), - const Text( - 'Personen', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Home', - icon: Column( - children: [ - IconButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StartPage( - client: widget.client, - ), - ), - ); - }, - icon: const Icon( - Icons.home, - color: Colors.white, - ), - ), - const Text( - 'Home', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - icon: const Icon( - Icons.menu, - color: Colors.white, - ), - ), - ) - ] - : [ - BottomNavigationBarItem( - label: 'back', - backgroundColor: Colors.white, - icon: IconButton( - onPressed: () {}, - icon: const Icon( - Icons.arrow_back, - color: Colors.white, - ), - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, - ), - ), - ), - ], - ); - }, - ), + // bottomNavigationBar: Builder( + // builder: (context) { + // return BottomBar( + // children: widget.client.session.accessToken != null + // ? [ + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Personen', + // icon: Column( + // children: [ + // IconButton( + // onPressed: () => + // Scaffold.of(context).openDrawer(), + // icon: const Icon( + // Icons.group, + // color: Colors.white, + // ), + // ), + // const Text( + // 'Personen', + // style: TextStyle( + // color: Colors.white, + // fontSize: 16, + // ), + // ) + // ], + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Home', + // icon: Column( + // children: [ + // IconButton( + // onPressed: () { + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => StartPage( + // client: widget.client, + // ), + // ), + // ); + // }, + // icon: const Icon( + // Icons.home, + // color: Colors.white, + // ), + // ), + // const Text( + // 'Home', + // style: TextStyle( + // color: Colors.white, + // fontSize: 16, + // ), + // ) + // ], + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Menu', + // icon: IconButton( + // onPressed: () { + // Scaffold.of(context).openDrawer(); + // }, + // icon: const Icon( + // Icons.menu, + // color: Colors.white, + // ), + // ), + // ) + // ] + // : [ + // BottomNavigationBarItem( + // label: 'back', + // backgroundColor: Colors.white, + // icon: IconButton( + // onPressed: () {}, + // icon: const Icon( + // Icons.arrow_back, + // color: Colors.white, + // ), + // ), + // ), + // BottomNavigationBarItem( + // backgroundColor: Colors.white, + // label: 'Menu', + // icon: IconButton( + // onPressed: () => Scaffold.of(context).openDrawer(), + // icon: const Icon( + // Icons.menu, + // color: Colors.white, + // ), + // ), + // ), + // ], + // ); + // }, + // ), body: !_loading ? Background( child: Center( diff --git a/frontend/app/lib/pages/login_page.dart b/frontend/app/lib/pages/login_page.dart index daf5e9b..b7d49d2 100644 --- a/frontend/app/lib/pages/login_page.dart +++ b/frontend/app/lib/pages/login_page.dart @@ -1,4 +1,5 @@ import 'package:app/gapi/client.dart'; +import 'package:app/model/services/backend_service.dart'; import 'package:app/pages/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; @@ -12,11 +13,11 @@ import 'package:grpc/grpc.dart'; class LoginPage extends StatefulWidget { const LoginPage({ super.key, - required this.client, + // required this.client, // required this.onChangePage, }); - final GClient client; + // final GClient client; // void Function(Pages page) onChangePage; @override @@ -87,7 +88,7 @@ class _LoginPageState extends State { label: 'back', backgroundColor: Colors.white, icon: IconButton( - onPressed: () => Navigator.of(context).pop(widget.client), + onPressed: () => Navigator.of(context).pop(), icon: const Icon( Icons.arrow_back, color: Colors.white, @@ -250,73 +251,23 @@ class _LoginPageState extends State { if (_formKey.currentState!.validate()) { // final navigator = Navigator.of(context); _setLoading(true); - widget.client - .login( + BackendService.login( email: mailController.text, password: passwordController.text, - onError: ({GrpcError? error}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: error != null - ? Text( - 'Fehler: ${error.message}', - textAlign: - TextAlign.center, - style: - const TextStyle( - color: Colors - .black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign.center, - style: TextStyle( - color: - Colors.black), - ), - icon: const Icon( - Icons.error, - color: Colors.black, - ), - ); - }, - ); - }), - )); - }, - onSuccess: () { - // _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text('Login erfolgreich'), - )); - }, - ) - .then( + ).then( (r) { - if (r.accessToken != '') { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (ctx) => StartPage( - client: widget.client, - ), - ), - (ctx) => false, - ); + if (r) { + Navigator.pop(context); + Navigator.pop(context); + // Navigator.pushAndRemoveUntil( + // context, + // MaterialPageRoute( + // builder: (ctx) => const StartPage( + // // client: widget.client, + // ), + // ), + // (ctx) => false, + // ); // widget.onChangePage( // Pages.dashboard, // ); diff --git a/frontend/app/lib/pages/register_page.dart b/frontend/app/lib/pages/register_page.dart index 1257b00..d5ee106 100644 --- a/frontend/app/lib/pages/register_page.dart +++ b/frontend/app/lib/pages/register_page.dart @@ -10,10 +10,10 @@ import 'package:grpc/grpc.dart'; class RegisterPage extends StatefulWidget { const RegisterPage({ super.key, - required this.client, + // required this.client, }); - final GClient client; + // final GClient client; @override State createState() => _RegisterPageState(); @@ -112,7 +112,7 @@ class _RegisterPageState extends State { label: 'back', backgroundColor: Colors.white, icon: IconButton( - onPressed: () => Navigator.of(context).pop(widget.client), + onPressed: () => Navigator.of(context).pop(), icon: const Icon( Icons.arrow_back, color: Colors.white, @@ -209,143 +209,7 @@ class _RegisterPageState extends State { ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { - _setLoading(true); - widget.client - .createAccount( - email: mailController.text, - password: passwordController.text, - onError: ({GrpcError? error}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: error != null - ? Text( - 'Fehler: ${error.message}', - textAlign: - TextAlign - .center, - style: const TextStyle( - color: Colors - .black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign - .center, - style: TextStyle( - color: Colors - .black), - ), - icon: const Icon( - Icons.error, - color: Colors.black, - ), - ); - }, - ); - }), - )); - }) - .then( - (r) { - if (r.account.secretKey != '') { - widget.client - .login( - email: mailController.text, - password: - passwordController.text, - onError: ( - {GrpcError? error}) {}, - onSuccess: () {}) - .then((resp) { - widget.client.getAccount( - accountId: r.account.id, - onError: ({GrpcError? err}) { - _setLoading(false); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: const Text( - 'Login fehlgeschlagen', - ), - action: SnackBarAction( - textColor: Colors.grey, - label: 'Details', - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - content: err != - null - ? Text( - 'Hoppla! Da ist etwas schief gelaufen..\n\n${err.message}', - textAlign: - TextAlign.center, - style: const TextStyle( - color: - Colors.black), - ) - : const Text( - 'Interner Fehler', - textAlign: - TextAlign.center, - style: TextStyle( - color: - Colors.black), - ), - icon: - const Icon( - Icons.error, - color: Colors - .black, - ), - ); - }, - ); - }), - ), - ); - }); - }); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (builder) => StartPage( - client: widget.client)), - (route) => false); - showDialog( - context: context, - builder: (builder) { - return const AlertDialog( - content: Text( - 'Account wurde angelegt', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black), - ), - icon: Icon( - Icons.done, - size: 50, - ), - iconColor: Colors.green, - ); - }); - } - }, - ); + // _setLoading(true); } }, child: const Icon(Icons.login)) diff --git a/frontend/app/lib/pages/start_page.dart b/frontend/app/lib/pages/start_page.dart index 751b9dd..39d6ecf 100644 --- a/frontend/app/lib/pages/start_page.dart +++ b/frontend/app/lib/pages/start_page.dart @@ -1,21 +1,26 @@ import 'package:app/gapi/client.dart'; +import 'package:app/model/apis/api_response.dart'; +import 'package:app/model/view_model/account_vm.dart'; import 'package:app/pages/dashboard_page.dart'; import 'package:app/pages/login_page.dart'; import 'package:app/pages/register_page.dart'; +import 'package:app/pb/account.pb.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; import 'dart:core'; +import 'package:provider/provider.dart'; + // ignore: must_be_immutable class StartPage extends StatefulWidget { - StartPage({ + const StartPage({ super.key, - required this.client, + // required this.client, }); - GClient client; + // GClient client; @override State createState() => _StartPageState(); @@ -24,320 +29,344 @@ class StartPage extends StatefulWidget { class _StartPageState extends State { final List bottombarButtons = []; - void _updateClient(GClient c) { - setState(() { - widget.client = c; - }); - } + // void _updateClient(GClient c) { + // setState(() { + // widget.client = c; + // }); + // } @override void initState() { super.initState(); } + SnackBar _snackBar(BuildContext context, String message, String label, + void Function() action) { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + // ScaffoldMessenger.of(context).clearSnackBars(); + return SnackBar( + content: Text( + message, + style: const TextStyle(color: Colors.black), + ), + backgroundColor: Colors.white, + action: SnackBarAction( + label: label, + onPressed: action, + ), + ); + } + @override Widget build(BuildContext context) { return Background( - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - ), - drawer: Builder(builder: (context) { - return SideDrawer(children: [ - const Spacer(), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'About', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.question_answer, - color: Colors.white, - ), - ], - ), + child: ChangeNotifierProvider( + create: (context) => AccountViewModel(), + child: Consumer(builder: (context, value, child) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, ), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'Datenschutz', - style: TextStyle(fontSize: 20), + drawer: Builder(builder: (context) { + return SideDrawer(children: [ + const Spacer(), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'About', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.question_answer, + color: Colors.white, + ), + ], ), - Spacer(), - Icon( - Icons.privacy_tip, - color: Colors.white, + ), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'Datenschutz', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.privacy_tip, + color: Colors.white, + ), + ], ), - ], - ), - ), - TextButton( - onPressed: () { - Scaffold.of(context).closeDrawer(); - }, - child: const Row( - children: [ - Text( - 'Impressum', - style: TextStyle(fontSize: 20), + ), + TextButton( + onPressed: () { + Scaffold.of(context).closeDrawer(); + }, + child: const Row( + children: [ + Text( + 'Impressum', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.apartment, + color: Colors.white, + ), + ], ), - Spacer(), - Icon( - Icons.apartment, - color: Colors.white, + ), + TextButton( + onPressed: () { + value.logout(); + // ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + _snackBar( + context, + value.response.message != null + ? value.response.message! + : value.response.status.toString(), + value.response.status.toString(), + () { + print('asdf'); + }, + ), + ); + }, + child: const Row( + children: [ + Text( + 'Log out', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Icon( + Icons.logout, + color: Colors.white, + ), + ], ), - ], - ), - ), - TextButton( - onPressed: () { - setState(() { - widget.client.session.accessToken = null; - widget.client.session - .removeSession(widget.client.session.sessionId!); - }); - }, - child: const Row( - children: [ - Text( - 'Log out', - style: TextStyle(fontSize: 20), - ), - Spacer(), - Icon( - Icons.logout, - color: Colors.white, - ), - ], - ), - ), - const SizedBox( - height: 250, - ) - ]); - }), - bottomNavigationBar: Builder(builder: (context) { - return BottomBar( - // onTap: (value) => _bottomBarAction(value), - children: widget.client.session.accessToken != null - ? [ - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Personen', - icon: Column( - children: [ - IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), + ), + const SizedBox( + height: 250, + ) + ]); + }), + bottomNavigationBar: Builder(builder: (context) { + return BottomBar( + // onTap: (value) => _bottomBarAction(value), + children: value.response.data != null + ? [ + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Personen', + icon: Column( + children: [ + IconButton( + onPressed: () => + Scaffold.of(context).openDrawer(), + icon: const Icon( + Icons.group, + color: Colors.white, + ), + ), + const Text( + 'Personen', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Dashboard', + icon: Column( + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.dashboard, + color: Colors.white, + ), + ), + const Text( + 'Dashboard', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Menu', + icon: IconButton( + onPressed: () { + Scaffold.of(context).openDrawer(); + }, icon: const Icon( - Icons.group, + Icons.menu, color: Colors.white, ), ), - const Text( - 'Personen', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Dashboard', - icon: Column( - children: [ - IconButton( - onPressed: () async { - if (widget.client.session.accessTokenExpiresAt! - .toDateTime() - .isBefore(DateTime.now())) { - await widget.client.refreshToken(); - } - if (!widget.client.isLoggedIn && - context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sitzung ist abgelaufen.'), - ), - ); - if (context.mounted) { - Navigator.pushAndRemoveUntil( + ) + ] + : [ + BottomNavigationBarItem( + label: 'register', + backgroundColor: Colors.white, + icon: Column( + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.login, + color: Colors.white, + ), + ), + const Text( + 'Registrieren', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + label: 'login', + backgroundColor: Colors.white, + icon: Column( + children: [ + IconButton( + onPressed: () { + Navigator.push( context, MaterialPageRoute( - builder: (context) => - LoginPage(client: widget.client)), - (route) => false); - } - } else { - final ctx = context; - // ignore: use_build_context_synchronously - GClient c = await Navigator.push( - ctx, - MaterialPageRoute( - builder: (context) => DashboardPage( - client: widget.client, - ), - ), - ); - _updateClient(c); - } - }, + builder: (builder) => + const LoginPage())); + }, + icon: const Icon( + Icons.login, + color: Colors.white, + ), + ), + const Text( + 'Login', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ) + ], + ), + ), + BottomNavigationBarItem( + backgroundColor: Colors.white, + label: 'Menu', + icon: IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), icon: const Icon( - Icons.dashboard, + Icons.menu, color: Colors.white, ), ), + ), + ], + ); + }), + body: Column( + children: [ + if (value.response.status == Status.COMPLETED && + value.response.data != null && + !(value.response.data as Account).emailVerified) + Card( + color: Colors.orange, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ const Text( - 'Dashboard', + 'E-Mail ist noch nicht verifiziert.', style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'sans-serif', + fontSize: 14), + ), + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.restore, color: Colors.white, - fontSize: 16, ), - ) + ), ], ), ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - icon: const Icon( - Icons.menu, - color: Colors.white, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Image( + image: AssetImage( + 'lib/assets/logo_300x200.png', ), ), - ) - ] - : [ - BottomNavigationBarItem( - label: 'register', - backgroundColor: Colors.white, - icon: Column( - children: [ - IconButton( - onPressed: () async { - widget.client = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RegisterPage( - client: widget.client, - ))); - }, - icon: const Icon( - Icons.login, - color: Colors.white, - ), - ), - const Text( - 'Registrieren', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], + const SizedBox( + height: 40, ), - ), - BottomNavigationBarItem( - label: 'login', - backgroundColor: Colors.white, - icon: Column( - children: [ - IconButton( - onPressed: () async { - final c = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LoginPage( - client: widget.client, - ), - )); - _updateClient(c); - }, - icon: const Icon( - Icons.login, - color: Colors.white, - ), - ), - const Text( - 'Login', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ) - ], - ), - ), - BottomNavigationBarItem( - backgroundColor: Colors.white, - label: 'Menu', - icon: IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, + 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, ), ), - ), - ], - ); - }), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Image( - image: AssetImage( - 'lib/assets/logo_300x200.png', - ), - ), - if (widget.client.account != null && - !widget.client.account!.emailVerified) - Container( - height: 120, - width: double.infinity, - padding: - const EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Card( - color: Colors.brown.shade300, - child: const Text( - 'Deine E-Mail Adresse ist noch nicht validiert.'), + TextButton( + onPressed: () { + // ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + _snackBar( + context, + value.response.message != null + ? value.response.message! + : value.response.status.toString(), + value.response.status.toString(), + () { + print('asdf'); + }, + ), + ); + }, + child: const Text('asdf')) + ], ), ), - 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/pubspec.lock b/frontend/app/pubspec.lock index 9323a01..112b12d 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -195,6 +195,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: "direct main" description: @@ -219,6 +227,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" sky_engine: dependency: transitive description: flutter diff --git a/frontend/app/pubspec.yaml b/frontend/app/pubspec.yaml index f28d267..2960757 100644 --- a/frontend/app/pubspec.yaml +++ b/frontend/app/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: sqflite: ^2.3.0 path: ^1.8.3 fixnum: ^1.1.0 + provider: ^6.0.5 dev_dependencies: lints: ^2.0.0