Add functional login page

This commit is contained in:
2024-12-20 23:30:05 +01:00
parent f530a52e9d
commit d5d5bc6e5d
13 changed files with 356 additions and 144 deletions

View File

@@ -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<bool> 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<dynamic>)
.map((sheet) => Sheet.fromJson(sheet as Map<String, dynamic>))
.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;

101
lib/home_page.dart Normal file
View File

@@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ApiClient? apiClient;
Future<bool> apiLoggedIn = Future.value(false);
final StorageHelper _storageHelper = StorageHelper();
@override
void initState() {
super.initState();
}
// Future<String?> 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<List<Sheet>> 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<List<Sheet>> 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());
}
}),
);
}
}

1
lib/login.dart Normal file
View File

@@ -0,0 +1 @@

125
lib/login_page.dart Normal file
View File

@@ -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<LoginPage> {
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<void> _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<bool> _validateJwt(String jwt) async {
try {
bool expired = JwtDecoder.isExpired(jwt);
return !expired;
} on FormatException {
return false;
}
}
Future<void> _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'),
),
],
),
),
);
}
}

View File

@@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ApiClient apiClient = ApiClient();
Future<bool> apiLoggedIn = Future.value(false);
@override
void initState() {
super.initState();
apiLoggedIn = apiClient.login("admin@admin.com", "sheetable");
}
// Future<String?> 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<List<Sheet>> 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<List<Sheet>> 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(),
);
}
}

22
lib/storage_helper.dart Normal file
View File

@@ -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<String?> read(StorageKey key) {
return secureStorage.read(key: key.name);
}
Future<void> write(StorageKey key, String value) {
return secureStorage.write(key: key.name, value: value);
}
}