From ef641d4023bc9328d8a4b04932545d323015dbe0 Mon Sep 17 00:00:00 2001 From: Alzalia Date: Fri, 8 Aug 2025 01:03:48 +0200 Subject: [PATCH] feat: added authentification and redirection --- android/app/src/main/AndroidManifest.xml | 4 +- android/app/src/main/res/xml/backup_rules.xml | 4 + lib/config/constants.dart | 1 + lib/config/dependencies.dart | 21 +- lib/data/repositories/auth_repository.dart | 43 +++ lib/data/repositories/owner_repository.dart | 26 +- lib/data/services/api_client.dart | 55 +++- lib/data/services/auth_client.dart | 77 ++++++ lib/data/services/websocket_client.dart | 12 +- lib/main.dart | 9 +- lib/routing/router.dart | 34 ++- lib/routing/routes.dart | 3 + .../add_page/view_model/add_view_model.dart | 95 ++++--- lib/ui/add_page/widgets/add_page.dart | 245 +++++++++--------- lib/ui/add_page/widgets/owner_popup.dart | 9 +- lib/ui/auth/viewmodel/login_view_model.dart | 24 ++ lib/ui/auth/widgets/login_page.dart | 92 +++++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 136 ++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 24 files changed, 731 insertions(+), 173 deletions(-) create mode 100644 android/app/src/main/res/xml/backup_rules.xml create mode 100644 lib/config/constants.dart create mode 100644 lib/data/repositories/auth_repository.dart create mode 100644 lib/data/services/auth_client.dart create mode 100644 lib/ui/auth/viewmodel/login_view_model.dart create mode 100644 lib/ui/auth/widgets/login_page.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1aa6892..5baa3ee 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,9 @@ + android:icon="@mipmap/ic_launcher" + android:allowBackup="true" + android:fullBackupContent="@xml/backup_rules"> + + + \ No newline at end of file diff --git a/lib/config/constants.dart b/lib/config/constants.dart new file mode 100644 index 0000000..4a189e7 --- /dev/null +++ b/lib/config/constants.dart @@ -0,0 +1 @@ +const apiBasePath = "bal.ueauvergne.fr"; diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 2644faf..3236d47 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -1,8 +1,23 @@ -import "package:flutter/widgets.dart"; -import "package:nested/nested.dart"; import "package:provider/provider.dart"; +import "package:provider/single_child_widget.dart"; +import "package:seshat/data/repositories/auth_repository.dart"; + +import "package:seshat/data/repositories/owner_repository.dart"; +import "package:seshat/data/services/api_client.dart"; +import "package:seshat/data/services/auth_client.dart"; import "package:seshat/data/services/websocket_client.dart"; List get providers { - return [Provider(create: (context) => WebsocketClient())]; + return [ + Provider(create: (context) => AuthClient()), + Provider(create: (context) => ApiClient(authClient: context.read())), + Provider(create: (context) => WebsocketClient()), + Provider( + create: (context) => + OwnerRepository(apiClient: context.read(), wsClient: context.read()), + ), + ChangeNotifierProvider( + create: (context) => AuthRepository(authClient: context.read()), + ), + ]; } diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart new file mode 100644 index 0000000..439d219 --- /dev/null +++ b/lib/data/repositories/auth_repository.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; +import 'package:seshat/data/services/auth_client.dart'; +import 'package:seshat/utils/result.dart'; + +class AuthRepository extends ChangeNotifier { + AuthRepository({required AuthClient authClient}) : _authClient = authClient; + + final AuthClient _authClient; + + bool? _isAuthenticated; + + Future get isLoggedIn async { + if (_isAuthenticated != null) { + return _isAuthenticated!; + } + final result = await _authClient.hasValidToken(); + switch (result) { + case Ok(): + if (result.value) { + return true; + } + return false; + case Error(): + return false; + } + } + + Future> login(String username, String password) async { + try { + final result = await _authClient.login(username, password); + switch (result) { + case Ok(): + _isAuthenticated = true; + return Result.ok(()); + case Error(): + return Result.error(result.error); + } + } catch (e, stackTrace) { + debugPrintStack(stackTrace: stackTrace); + return Result.error(Exception(e)); + } + } +} diff --git a/lib/data/repositories/owner_repository.dart b/lib/data/repositories/owner_repository.dart index d3de6e7..f00ea5d 100644 --- a/lib/data/repositories/owner_repository.dart +++ b/lib/data/repositories/owner_repository.dart @@ -14,32 +14,42 @@ class OwnerRepository { final ApiClient _apiClient; final WebsocketClient _wsClient; - List? _cachedData; + List? _cachedOwners; + + Future> postOwner( + String firstName, + String lastName, + String contact, + ) async { + return Result.ok( + Owner(firstName: firstName, lastName: lastName, contact: contact, id: 50), + ); + } Future>> getOwners() async { - if (_cachedData == null) { + if (_cachedOwners == null) { final result = await _apiClient.getOwners(); if (result is Ok>) { - _cachedData = result.value; + _cachedOwners = result.value; } return result; } else { - return Result.ok(_cachedData!); + return Result.ok(_cachedOwners!); } } Stream liveOwners() async* { - await for (String data in _wsClient.connect()) { + await for (String data in await _wsClient.connect()) { Map decodedData = jsonDecode( data, ).cast>(); Owner owner = Owner.fromJSON(decodedData); - if (_cachedData == null) { - getOwners(); + if (_cachedOwners == null) { + await getOwners(); } else { - _cachedData!.add(owner); + _cachedOwners!.add(owner); yield* Stream.value(owner); } } diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index 22d6cbc..24a1738 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -1,8 +1,61 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:seshat/config/constants.dart'; +import 'package:seshat/data/services/auth_client.dart'; import 'package:seshat/domain/models/owner.dart'; +import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; +typedef AuthHeaderProvider = String? Function(); + class ApiClient { + ApiClient({ + String? host, + int? port, + HttpClient Function()? clientFactory, + required AuthClient authClient, + }) : _authClient = authClient; + + final AuthClient _authClient; + late final Command0 load; + String? token; + bool isReady = false; + FlutterSecureStorage? _secureStorage; + + Future _initStore() async { + _secureStorage ??= const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); + } + Future>> getOwners() async { - return Result.ok([]); + final client = HttpClient(); + try { + await _initStore(); + final request = await client.getUrl( + Uri.parse("https://$apiBasePath/owners"), + ); + final token = await _secureStorage!.read(key: "token"); + debugPrint("\n\n\n\nFOUND TOKEN : $token\n\n\n\n"); + // await _authHeader(request.headers); + request.headers.add(HttpHeaders.authorizationHeader, "Bearer $token"); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(Utf8Decoder()).join(); + final json = jsonDecode(stringData) as List; + return Result.ok( + json.map((element) => Owner.fromJSON(element)).toList(), + ); + } else { + return const Result.error(HttpException("Invalid response")); + } + } on Exception catch (error) { + return Result.error(error); + } finally { + client.close(); + } } } diff --git a/lib/data/services/auth_client.dart b/lib/data/services/auth_client.dart new file mode 100644 index 0000000..f451d6d --- /dev/null +++ b/lib/data/services/auth_client.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:seshat/config/constants.dart'; +import 'package:seshat/utils/result.dart'; +import "package:http/http.dart" as http; + +class AuthClient { + AuthClient(); + FlutterSecureStorage? _secureStorage; + + Future _initStore() async { + _secureStorage ??= const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); + } + + Future> hasValidToken() async { + try { + await _initStore(); + bool hasToken = await _secureStorage!.containsKey(key: "token"); + debugPrint("\n\n\n${hasToken == true} => HAS_TOKEN\n\n\n"); + if (hasToken) { + var token = await _secureStorage!.read(key: "token"); + var url = Uri.parse("https://$apiBasePath/token-check"); + var response = await http.post( + url, + headers: {"Content-Type": "application/json"}, + body: jsonEncode({"token": token}), + ); + debugPrint( + "\n\n\n${response.body is String} => ${response.body}\n\n\n", + ); + + if (response.body == "true") { + return Result.ok(true); + } + } + return Result.ok(false); + } catch (e) { + debugPrint(e.toString()); + return Result.error(Exception(e)); + } + } + + Future> login(String username, String password) async { + var client = http.Client(); + try { + await _initStore(); + var url = Uri.parse("https://$apiBasePath/auth"); + var response = await client.post( + url, + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + body: jsonEncode({"password": password, "username": username}), + ); + if (response.statusCode == 200) { + var json = jsonDecode(response.body); + await _secureStorage!.write(key: "token", value: json["access_token"]); + return Result.ok(json["access_token"]); + } else if (response.statusCode == 401) { + return Result.error(Exception("Wrong credentials")); + } else { + return Result.error(Exception("Token creation error")); + } + } catch (e, stackTrace) { + debugPrint(e.toString()); + debugPrintStack(stackTrace: stackTrace); + return Result.error(Exception(e)); + } finally { + client.close(); + } + } +} diff --git a/lib/data/services/websocket_client.dart b/lib/data/services/websocket_client.dart index 7e1272b..e2b87ba 100644 --- a/lib/data/services/websocket_client.dart +++ b/lib/data/services/websocket_client.dart @@ -1,10 +1,16 @@ +import 'package:seshat/config/constants.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class WebsocketClient { - static const String _host = "ws://bal.ninjdai.fr:3000"; + Future> connect() async { + final channel = WebSocketChannel.connect( + Uri.parse("wss://$apiBasePath/ws"), + ); + + await channel.ready; + + channel.sink.add("json-token: "); - Stream connect() { - final channel = WebSocketChannel.connect(Uri.parse("$_host/ws")); return channel.stream; } } diff --git a/lib/main.dart b/lib/main.dart index 2eed0bf..d05f6a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; +import 'package:seshat/config/dependencies.dart'; import 'package:seshat/routing/router.dart'; void main() { Logger.root.level = Level.ALL; + WidgetsFlutterBinding.ensureInitialized(); - // runApp(MultiProvider(providers: providers, child: const MyApp())); - runApp(const MyApp()); + runApp(MultiProvider(providers: providers, child: const MyApp())); + // runApp(const MyApp()); } class MyApp extends StatelessWidget { @@ -15,7 +18,7 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp.router(routerConfig: router()); + return MaterialApp.router(routerConfig: router(context.read())); } } diff --git a/lib/routing/router.dart b/lib/routing/router.dart index bdd87a2..6f56d6a 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -1,12 +1,31 @@ import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:seshat/data/repositories/auth_repository.dart'; import 'package:seshat/routing/routes.dart'; import 'package:seshat/ui/add_page/view_model/add_view_model.dart'; import 'package:seshat/ui/add_page/widgets/add_page.dart'; +import 'package:seshat/ui/auth/viewmodel/login_view_model.dart'; +import 'package:seshat/ui/auth/widgets/login_page.dart'; import 'package:seshat/ui/home_page/home_page.dart'; import 'package:seshat/ui/sell_page/sell_page.dart'; -GoRouter router() => GoRouter( +GoRouter router(AuthRepository authRepository) => GoRouter( initialLocation: Routes.add, + redirect: (context, state) async { + final loggedIn = await context.read().isLoggedIn; + final logginIn = state.matchedLocation == Routes.login; + + if (!loggedIn) { + return Routes.login; + } + + if (logginIn) { + return Routes.add; + } + + return null; + }, + refreshListenable: authRepository, routes: [ GoRoute( path: Routes.home, @@ -14,8 +33,10 @@ GoRouter router() => GoRouter( routes: [ GoRoute( path: Routes.add, - pageBuilder: (context, state) => - NoTransitionPage(child: AddPage(viewModel: AddViewModel())), + pageBuilder: (context, state) { + final viewModel = AddViewModel(ownerRepository: context.read()); + return NoTransitionPage(child: AddPage(viewModel: viewModel)); + }, // routes: [ // GoRoute(path: Routes.addForm), // GoRoute(path: Routes.addOwner), @@ -26,6 +47,13 @@ GoRouter router() => GoRouter( path: Routes.sell, pageBuilder: (context, state) => NoTransitionPage(child: SellPage()), ), + GoRoute( + path: Routes.login, + pageBuilder: (context, state) { + final viewModel = LoginViewModel(authRepository: context.read()); + return NoTransitionPage(child: LoginPage(viewModel: viewModel)); + }, + ), ], ), ], diff --git a/lib/routing/routes.dart b/lib/routing/routes.dart index 23ae1dd..301929d 100644 --- a/lib/routing/routes.dart +++ b/lib/routing/routes.dart @@ -10,4 +10,7 @@ abstract final class Routes { // ==[ SELL ]== static const sell = '/sell'; + + // ==[ AUTH ]== + static const login = '/login'; } diff --git a/lib/ui/add_page/view_model/add_view_model.dart b/lib/ui/add_page/view_model/add_view_model.dart index 351bfd0..3a340c8 100644 --- a/lib/ui/add_page/view_model/add_view_model.dart +++ b/lib/ui/add_page/view_model/add_view_model.dart @@ -1,12 +1,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:seshat/data/repositories/owner_repository.dart'; import 'package:seshat/domain/models/book.dart'; import 'package:seshat/domain/models/owner.dart'; +import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; class AddViewModel extends ChangeNotifier { - AddViewModel(); + AddViewModel({required OwnerRepository ownerRepository}) + : _ownerRepository = ownerRepository { + load = Command0(_load)..execute(); + } + + final OwnerRepository _ownerRepository; /* * ==================== @@ -21,37 +28,37 @@ class AddViewModel extends ChangeNotifier { notifyListeners(); } - final List _owners = []; + List _owners = []; List? get owners => _owners; - Owner addOwner(String firstName, String lastName, String contact) { - if (_owners.isEmpty) { - _owners.add( - Owner( - firstName: firstName, - lastName: lastName, - contact: contact, - id: 1, - ), - ); - } else { - _owners.add( - Owner( - firstName: firstName, - lastName: lastName, - contact: contact, - id: _owners.last.id + 1, - ), - ); - } - notifyListeners(); - return Owner( - firstName: firstName, - lastName: lastName, - contact: contact, - id: 0, + Future> addOwner( + String firstName, + String lastName, + String contact, + ) async { + final result = await _ownerRepository.postOwner( + firstName, + lastName, + contact, ); + + switch (result) { + case Ok(): + final secondResult = await _ownerRepository.getOwners(); + + switch (secondResult) { + case Ok(): + _owners = secondResult.value; + _currentOwner = result.value; + notifyListeners(); + return Result.ok(result.value); + case Error(): + return Result.error(secondResult.error); + } + case Error(): + return Result.error(result.error); + } } /* @@ -87,8 +94,38 @@ class AddViewModel extends ChangeNotifier { ); } - /// Sens an api request with + /// Sends an api request with // Result newBookInstance() { // }; + + /* + * ================================= + * =====[ COMMAND AND LOADING ]===== + * ================================= +*/ + + late final Command0 load; + bool isLoaded = false; + + Future> _load() async { + return await _loadOwners(); + } + + Future> _loadOwners() async { + final result = await _ownerRepository.getOwners(); + switch (result) { + case Ok(): + _owners = result.value; + isLoaded = true; + case Error(): + debugPrint("Oupsie daysie, ${result.error}"); + } + notifyListeners(); + _ownerRepository.liveOwners().listen((Owner owner) { + _owners.add(owner); + notifyListeners(); + }); + return result; + } } diff --git a/lib/ui/add_page/widgets/add_page.dart b/lib/ui/add_page/widgets/add_page.dart index a4b95bd..b01e1f1 100644 --- a/lib/ui/add_page/widgets/add_page.dart +++ b/lib/ui/add_page/widgets/add_page.dart @@ -32,130 +32,137 @@ class _AddPageState extends State { // builder: (context, screen, child) { return Scaffold( bottomNavigationBar: AppNavigationBar(startIndex: 1), - body: Stack( - children: [ - ColoredBox(color: Colors.black), - MobileScanner( - controller: controller, - onDetect: (barcodes) async { - if (widget.viewModel.currentOwner == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Attention : vous devez choisir un·e propriétaire", - ), - behavior: SnackBarBehavior.floating, - ), - ); - return; - } - - void setPrice(num newPrice) async { - setState(() { - price = newPrice; - }); - } - - Result result = await widget.viewModel.scanBook(barcodes); - - switch (result) { - case Ok(): - await _confirmationDialogBuilder( - context, - setPrice, - controller, - widget.viewModel, - result.value, - ); - break; - case Error(): - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Erreur : ${result.error}"), - behavior: SnackBarBehavior.floating, - ), - ); - break; - } - }, - ), - SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Card( - margin: EdgeInsets.symmetric(horizontal: 50), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListenableBuilder( - listenable: widget.viewModel, - builder: (context, child) => ListTile( - leading: Icon(Icons.person), - title: TextButton( - child: Text( - (widget.viewModel.currentOwner == null) - ? "Aucun" - : "${widget.viewModel.currentOwner!.firstName} ${widget.viewModel.currentOwner!.lastName}", - ), - onPressed: () => _ownerDialogBuilder( - context, - controller, - widget.viewModel, - ), - ), - ), - ), - ListTile( - leading: Icon(Icons.attach_money), - title: TextButton( - child: Text( - (widget.viewModel.askPrice) - ? "Demander à chaque fois" - : "Prix libre toujours", - ), - onPressed: () { - setState(() { - widget.viewModel.askPrice = - !widget.viewModel.askPrice; - }); - }, - ), - ), - ], + body: ListenableBuilder( + listenable: widget.viewModel, + builder: (context, child) => switch (widget.viewModel.isLoaded) { + false => CircularProgressIndicator(), + true => Stack( + children: [ + ColoredBox(color: Colors.black), + MobileScanner( + controller: controller, + onDetect: (barcodes) async { + if (widget.viewModel.currentOwner == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Attention : vous devez choisir un·e propriétaire", + ), + behavior: SnackBarBehavior.floating, ), - ), - ), - SizedBox(height: 100), - SvgPicture.asset('assets/scan-overlay.svg'), - ], + ); + return; + } + + void setPrice(num newPrice) async { + setState(() { + price = newPrice; + }); + } + + Result result = await widget.viewModel.scanBook( + barcodes, + ); + + switch (result) { + case Ok(): + await _confirmationDialogBuilder( + context, + setPrice, + controller, + widget.viewModel, + result.value, + ); + break; + case Error(): + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Erreur : ${result.error}"), + behavior: SnackBarBehavior.floating, + ), + ); + break; + } + }, ), - ), - ), - SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Center( - child: TextButton( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(theme.cardColor), - ), - onPressed: () => _formDialogBuilder( - context, - controller, - widget.viewModel, - ), - child: Text("Enregistrer manuellement"), + SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Card( + margin: EdgeInsets.symmetric(horizontal: 50), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.person), + title: TextButton( + child: Text( + (widget.viewModel.currentOwner == null) + ? "Aucun" + : "${widget.viewModel.currentOwner!.firstName} ${widget.viewModel.currentOwner!.lastName}", + ), + onPressed: () => _ownerDialogBuilder( + context, + controller, + widget.viewModel, + ), + ), + ), + ListTile( + leading: Icon(Icons.attach_money), + title: TextButton( + child: Text( + (widget.viewModel.askPrice) + ? "Demander à chaque fois" + : "Prix libre toujours", + ), + onPressed: () { + setState(() { + widget.viewModel.askPrice = + !widget.viewModel.askPrice; + }); + }, + ), + ), + ], + ), + ), + ), + SizedBox(height: 100), + SvgPicture.asset('assets/scan-overlay.svg'), + ], ), ), - ], - ), + ), + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Center( + child: TextButton( + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + theme.cardColor, + ), + ), + onPressed: () => _formDialogBuilder( + context, + controller, + widget.viewModel, + ), + child: Text("Enregistrer manuellement"), + ), + ), + ], + ), + ), + ], ), - ], + }, ), ); // }, diff --git a/lib/ui/add_page/widgets/owner_popup.dart b/lib/ui/add_page/widgets/owner_popup.dart index 96f4b32..f6133a2 100644 --- a/lib/ui/add_page/widgets/owner_popup.dart +++ b/lib/ui/add_page/widgets/owner_popup.dart @@ -141,11 +141,14 @@ class _OwnerPopupState extends State { ), SizedBox(height: 10), ElevatedButton( - onPressed: () { + onPressed: () async { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); - widget.viewModel.currentOwner = widget.viewModel - .addOwner(firstName!, lastName!, contact!); + await widget.viewModel.addOwner( + firstName!, + lastName!, + contact!, + ); setState(() { showNewOwner = false; }); diff --git a/lib/ui/auth/viewmodel/login_view_model.dart b/lib/ui/auth/viewmodel/login_view_model.dart new file mode 100644 index 0000000..844e3e8 --- /dev/null +++ b/lib/ui/auth/viewmodel/login_view_model.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:seshat/data/repositories/auth_repository.dart'; +import 'package:seshat/utils/command.dart'; +import 'package:seshat/utils/result.dart'; + +class LoginViewModel extends ChangeNotifier { + LoginViewModel({required AuthRepository authRepository}) + : _authRepository = authRepository { + login = Command1(_login); + } + + final AuthRepository _authRepository; + + late Command1 login; + + Future> _login((String, String) credentials) async { + final (username, password) = credentials; + final result = await _authRepository.login(username, password); + if (result is Error) { + debugPrint("Hehe no"); + } + return result; + } +} diff --git a/lib/ui/auth/widgets/login_page.dart b/lib/ui/auth/widgets/login_page.dart new file mode 100644 index 0000000..f73bcad --- /dev/null +++ b/lib/ui/auth/widgets/login_page.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:seshat/routing/routes.dart'; +import 'package:seshat/ui/auth/viewmodel/login_view_model.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key, required this.viewModel}); + + final LoginViewModel viewModel; + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController _username = TextEditingController( + text: "ueauvergne", + ); + final TextEditingController _password = TextEditingController( + text: "ueauvergne", + ); + + @override + void initState() { + super.initState(); + widget.viewModel.login.addListener(_onResult); + } + + @override + void didUpdateWidget(covariant LoginPage oldWidget) { + super.didUpdateWidget(oldWidget); + oldWidget.viewModel.removeListener(_onResult); + widget.viewModel.login.addListener(_onResult); + } + + @override + void dispose() { + widget.viewModel.login.removeListener(_onResult); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextField(controller: _username), + TextField(controller: _password), + ListenableBuilder( + listenable: widget.viewModel.login, + builder: (context, child) { + return FilledButton( + onPressed: () { + widget.viewModel.login.execute(( + _username.value.text, + _password.value.text, + )); + }, + child: Text("Connexion"), + ); + }, + ), + ], + ), + ); + } + + void _onResult() { + if (widget.viewModel.login.completed) { + widget.viewModel.login.clearResult(); + context.go(Routes.add); + } + + if (widget.viewModel.login.error) { + widget.viewModel.login.clearResult(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Une erreur est survenue lors de la connexion."), + action: SnackBarAction( + label: "Réessayer", + onPressed: () => widget.viewModel.login.execute(( + _username.value.text, + _password.value.text, + )), + ), + ), + ); + } + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 898e689..c44ff01 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,12 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import mobile_scanner +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index ae7e3fb..1ca61e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" flutter: dependency: "direct main" description: flutter @@ -86,6 +94,54 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -128,6 +184,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -224,6 +288,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -232,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -373,6 +493,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 134c778..2e08d74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: http: ^1.4.0 web_socket_channel: ^3.0.3 nested: ^1.0.0 + flutter_secure_storage: ^9.2.4 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..4fc759c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST