diff --git a/lib/api.dart b/lib/api.dart index 2a9d6f4..a303056 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -1,24 +1,21 @@ import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; // For cache storage -import 'package:file/memory.dart'; import 'sheet.dart'; class ApiClient { - final String baseUrl = - 'http://localhost:8080/api'; // Replace with your API base URL - String? _token; // Holds the JWT token after login + final String baseUrl; + String? token; - /// Checks if the user is authenticated - bool get isAuthenticated => _token != null; + ApiClient({required this.baseUrl, this.token}); /// Login and store the JWT token Future login(String username, String password) async { - print("Logging in..."); + log("Logging in..."); try { final url = '$baseUrl/login'; final response = await http.post( @@ -31,22 +28,22 @@ class ApiClient { ); if (response.statusCode == 200) { - _token = jsonDecode(response.body); - print('Login successful, token: $_token'); + token = jsonDecode(response.body); + log('Login successful'); return true; } else { - print('Login failed: ${response.statusCode}, ${response.body}'); + log('Login failed: ${response.statusCode}, ${response.body}'); } } catch (e) { - print('Error during login: $e'); + log('Error during login: $e'); } return false; } /// Logout and clear the token void logout() { - _token = null; - print('Logged out successfully.'); + token = null; + log('Logged out successfully.'); } /// Make a GET request @@ -54,7 +51,7 @@ class ApiClient { try { final url = '$baseUrl$endpoint'; final headers = { - 'Authorization': 'Bearer $_token', + 'Authorization': 'Bearer $token', if (!isBinary) 'Content-Type': 'application/json', }; @@ -63,10 +60,10 @@ class ApiClient { if (response.statusCode == 200) { return response; } else { - print('GET request failed: ${response.statusCode} ${response.body}'); + log('GET request failed: ${response.statusCode} ${response.body}'); } } catch (e) { - print('Error during GET request: $e'); + log('Error during GET request: $e'); } return null; } @@ -77,7 +74,7 @@ class ApiClient { try { final url = '$baseUrl$endpoint'; final headers = { - 'Authorization': 'Bearer $_token', + 'Authorization': 'Bearer $token', 'Content-Type': 'application/json', }; @@ -90,10 +87,10 @@ class ApiClient { if (response.statusCode == 200 || response.statusCode == 201) { return response; } else { - print('POST request failed: ${response.statusCode} ${response.body}'); + log('POST request failed: ${response.statusCode} ${response.body}'); } } catch (e) { - print('Error during POST request: $e'); + log('Error during POST request: $e'); } return null; } @@ -103,7 +100,7 @@ class ApiClient { try { final url = '$baseUrl$endpoint'; final headers = { - 'Authorization': 'Bearer $_token', + 'Authorization': 'Bearer $token', 'Content-Type': 'application/x-www-form-urlencoded', }; @@ -116,11 +113,10 @@ class ApiClient { if (response.statusCode == 200 || response.statusCode == 201) { return response; } else { - print( - 'POST Form Data request failed: ${response.statusCode} ${response.body}'); + log('POST Form Data request failed: ${response.statusCode} ${response.body}'); } } catch (e) { - print('Error during POST Form Data request: $e'); + log('Error during POST Form Data request: $e'); } return null; } @@ -133,27 +129,24 @@ class ApiClient { "sort_by": sortBy, }; - print("doing post..."); final response = await postFormData("/sheets", jsonEncode(bodyFormData)); - print("got response..."); if (response == null) { - print("Empty reponse"); return List.empty(); } if (response.statusCode == 200) { final data = jsonDecode(response.body); - print("Data: $data"); + log("Data: $data"); return (data['rows'] as List) .map((sheet) => Sheet.fromJson(sheet as Map)) .toList(); } else { - print('Failed to fetch sheets with status: ${response.statusCode}'); - print('Response: ${response.body}'); + log('Failed to fetch sheets with status: ${response.statusCode}'); + log('Response: ${response.body}'); } } catch (e) { - print('Error during fetching sheets: $e'); + log('Error during fetching sheets: $e'); } return List.empty(); @@ -163,42 +156,35 @@ class ApiClient { try { // Get the cache directory - print("Creating cache dir..."); // final cacheDir = kIsWeb // ? await MemoryFileSystem().systemTempDirectory.createTemp('cache') // : await getTemporaryDirectory(); final cacheDir = await getTemporaryDirectory(); final cachedPdfPath = '${cacheDir.path}/$sheetUuid.pdf'; - print("cache dir created"); - // Check if the file already exists in the cache final cachedFile = File(cachedPdfPath); - print("file created: $cachedFile"); if (await cachedFile.exists()) { - print("PDF found in cache: $cachedPdfPath"); + log("PDF found in cache: $cachedPdfPath"); return cachedFile; } // Make the authenticated API call - print("getting response"); final response = await this.get('/sheet/pdf/$sheetUuid', isBinary: true); - print("got response"); if (response != null && response.statusCode == 200) { // Save the fetched file to the cache // - print("writing file...: $cachedFile"); await cachedFile.writeAsBytes(response.bodyBytes); - print("PDF downloaded and cached at: $cachedPdfPath"); + log("PDF downloaded and cached at: $cachedPdfPath"); return cachedFile; } else { - print("Failed to fetch PDF from API. Status: ${response?.statusCode}"); + log("Failed to fetch PDF from API. Status: ${response?.statusCode}"); } } catch (e) { - print("Error fetching PDF: $e"); + log("Error fetching PDF: $e"); } return null; diff --git a/lib/home_page.dart b/lib/home_page.dart new file mode 100644 index 0000000..3dc8053 --- /dev/null +++ b/lib/home_page.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:sheetless/sheetview.dart'; +import 'package:sheetless/storage_helper.dart'; + +import 'api.dart'; +import 'sheet.dart'; + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + ApiClient? apiClient; + Future apiLoggedIn = Future.value(false); + final StorageHelper _storageHelper = StorageHelper(); + + @override + void initState() { + super.initState(); + } + + // Future getSafPickedSheetsDirectory() async { + // await Permission.storage.request(); + // await Permission.manageExternalStorage.request(); + // var pickedDirectories = await Saf.getPersistedPermissionDirectories(); + // if (pickedDirectories == null || pickedDirectories.isEmpty) { + // return null; + // } + // return pickedDirectories.last; + // } + + Future> acquireSheets() async { + final url = await _storageHelper.read(StorageKey.url); + final jwt = await _storageHelper.read(StorageKey.jwt); + apiClient = ApiClient(baseUrl: url!, token: jwt); + // TODO: check if really logged in + return await apiClient!.fetchSheets(); + // return api.main(); + + // final directory = await getApplicationDocumentsDirectory(); + // print("Directory is: $directory"); + // String? sheetsDirectory = "/home/julian/Klavier"; + // if (sheetsDirectory == null || sheetsDirectory.isEmpty) { + // await Saf.getDynamicDirectoryPermission(grantWritePermission: false); + // sheetsDirectory = "/home/julian/Klavier"; + // if (sheetsDirectory == null || sheetsDirectory.isEmpty) { + // throw Exception("No Directory selected"); + // } + // } + // return List.empty(); + + // var sheetsDirectoryFiles = await Saf.getFilesPathFor(sheetsDirectory); + // if (sheetsDirectoryFiles == null) { + // await Saf.releasePersistedPermissions(); + // throw Exception( + // "Permissions for directory no longer valid or Directory deleted. Please restart app."); + // } + // return loadSheetsSorted(sheetsDirectoryFiles); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("My Sheets"), + ), + body: FutureBuilder( + future: acquireSheets(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return SheetsWidget( + sheets: snapshot.data!, + callback: (sheet) => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SheetViewerPage( + sheet: sheet, + apiClient: apiClient!, + ), + ), + ), + ); + } else if (snapshot.hasError) { + return Center( + child: Text( + style: Theme.of(context) + .textTheme + .displaySmall! + .copyWith(color: Colors.red), + textAlign: TextAlign.center, + snapshot.error.toString())); + } else { + return const Center(child: CircularProgressIndicator()); + } + }), + ); + } +} diff --git a/lib/login.dart b/lib/login.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/login.dart @@ -0,0 +1 @@ + diff --git a/lib/login_page.dart b/lib/login_page.dart new file mode 100644 index 0000000..51b310b --- /dev/null +++ b/lib/login_page.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:sheetless/api.dart'; +import 'package:sheetless/home_page.dart'; +import 'package:sheetless/storage_helper.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController _urlController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + final StorageHelper _storageHelper = StorageHelper(); + String? _error; + + @override + void initState() { + super.initState(); + _checkJwtValidity(); + } + + Future _checkJwtValidity() async { + final jwt = await _storageHelper.read(StorageKey.jwt); + if (jwt != null) { + final isValid = await _validateJwt(jwt); + if (isValid) { + _navigateToMainPage(); + return; + } else { + final url = await _storageHelper.read(StorageKey.url); + final email = await _storageHelper.read(StorageKey.email); + final password = await _storageHelper.read(StorageKey.password); + if (url != null && email != null && password != null) { + _login(url, email, password); + } + } + } + } + + Future _validateJwt(String jwt) async { + try { + bool expired = JwtDecoder.isExpired(jwt); + return !expired; + } on FormatException { + return false; + } + } + + Future _login(String serverUrl, String email, String password) async { + setState(() { + _error = null; + }); + final apiClient = ApiClient(baseUrl: serverUrl); + final loginSuccessful = await apiClient.login(email, password); + if (loginSuccessful) { + await _storageHelper.write(StorageKey.url, serverUrl); + await _storageHelper.write(StorageKey.jwt, apiClient.token!); + await _storageHelper.write(StorageKey.email, email); + await _storageHelper.write(StorageKey.password, password); + _navigateToMainPage(); + } else { + // TODO: give more varied error messages + setState(() { + _error = "Login failed."; + }); + } + } + + void _navigateToMainPage() { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => MyHomePage()), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Login')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextField( + controller: _urlController, + decoration: InputDecoration(labelText: 'Url'), + ), + TextField( + controller: _emailController, + decoration: InputDecoration(labelText: 'Email'), + ), + TextField( + controller: _passwordController, + decoration: InputDecoration(labelText: 'Password'), + obscureText: true, + ), + if (_error != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(_error!, style: TextStyle(color: Colors.red)), + ), + SizedBox(height: 16), + ElevatedButton( + onPressed: () { + _login(_urlController.text, _emailController.text, + _passwordController.text); + }, + child: Text('Login'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3228db6..67e578a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:saf/saf.dart'; -import 'package:sheetless/sheetview.dart'; -import 'package:path_provider/path_provider.dart'; -import 'api.dart'; -import 'sheet.dart'; +import 'login_page.dart'; void main() { runApp(const MyApp()); @@ -21,103 +17,7 @@ class MyApp extends StatelessWidget { useMaterial3: true, primarySwatch: Colors.blue, ), - home: const MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - ApiClient apiClient = ApiClient(); - Future apiLoggedIn = Future.value(false); - - @override - void initState() { - super.initState(); - apiLoggedIn = apiClient.login("admin@admin.com", "sheetable"); - } - - // Future getSafPickedSheetsDirectory() async { - // await Permission.storage.request(); - // await Permission.manageExternalStorage.request(); - // var pickedDirectories = await Saf.getPersistedPermissionDirectories(); - // if (pickedDirectories == null || pickedDirectories.isEmpty) { - // return null; - // } - // return pickedDirectories.last; - // } - - Future> acquireSheets() async { - // await Permission.storage.request(); - // await Permission.manageExternalStorage.request(); - // - - await apiLoggedIn; // TODO: check if really logged in (returns bool) - return await apiClient.fetchSheets(); - // return api.main(); - - // final directory = await getApplicationDocumentsDirectory(); - // print("Directory is: $directory"); - // String? sheetsDirectory = "/home/julian/Klavier"; - // if (sheetsDirectory == null || sheetsDirectory.isEmpty) { - // await Saf.getDynamicDirectoryPermission(grantWritePermission: false); - // sheetsDirectory = "/home/julian/Klavier"; - // if (sheetsDirectory == null || sheetsDirectory.isEmpty) { - // throw Exception("No Directory selected"); - // } - // } - // return List.empty(); - - // var sheetsDirectoryFiles = await Saf.getFilesPathFor(sheetsDirectory); - // if (sheetsDirectoryFiles == null) { - // await Saf.releasePersistedPermissions(); - // throw Exception( - // "Permissions for directory no longer valid or Directory deleted. Please restart app."); - // } - // return loadSheetsSorted(sheetsDirectoryFiles); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("My Sheets"), - ), - body: FutureBuilder( - future: acquireSheets(), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasData) { - return SheetsWidget( - sheets: snapshot.data!, - callback: (sheet) => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SheetViewerPage( - sheet: sheet, - apiClient: apiClient, - ), - ), - ), - ); - } else if (snapshot.hasError) { - return Center( - child: Text( - style: Theme.of(context) - .textTheme - .displaySmall! - .copyWith(color: Colors.red), - textAlign: TextAlign.center, - snapshot.error.toString())); - } else { - return const Center(child: CircularProgressIndicator()); - } - }), + home: const LoginPage(), ); } } diff --git a/lib/storage_helper.dart b/lib/storage_helper.dart new file mode 100644 index 0000000..2704e21 --- /dev/null +++ b/lib/storage_helper.dart @@ -0,0 +1,22 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +enum StorageKey { url, jwt, email, password } + +class StorageHelper { + late FlutterSecureStorage secureStorage; + + StorageHelper() { + AndroidOptions getAndroidOptions() => const AndroidOptions( + encryptedSharedPreferences: true, + ); + secureStorage = FlutterSecureStorage(aOptions: getAndroidOptions()); + } + + Future read(StorageKey key) { + return secureStorage.read(key: key.name); + } + + Future write(StorageKey key, String value) { + return secureStorage.write(key: key.name, value: value); + } +} 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 e689288..db046a8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import device_info_plus +import flutter_secure_storage_macos import path_provider_foundation import pdfx import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 60072ca..73c2720 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,54 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + 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_test: dependency: "direct dev" description: flutter @@ -160,6 +208,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c3db8fc..1bd5ba0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: http: ^1.2.2 path_provider: ^2.1.5 flutter_cache_manager: ^3.4.1 + flutter_secure_storage: ^9.2.2 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index aeef807..1513f40 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); PdfxPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PdfxPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 49667b4..326b594 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 pdfx permission_handler_windows )