Files
sheetless/lib/login_page.dart
2026-01-24 19:22:03 +01:00

160 lines
4.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:logging/logging.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
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final log = Logger("_LoginPageState");
final TextEditingController _urlController = TextEditingController(
text: "https://sheetable.julian-mutter.de",
);
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final StorageHelper _storageHelper = StorageHelper();
String? _error;
bool loggingIn = false;
@override
void initState() {
super.initState();
_restoreStoredValues();
}
Future<void> _restoreStoredValues() async {
final jwt = await _storageHelper.readSecure(SecureStorageKey.jwt);
if (jwt != null && await _isJwtValid(jwt)) {
await _navigateToMainPage();
return;
}
final url = await _storageHelper.readSecure(SecureStorageKey.url);
final username = await _storageHelper.readSecure(SecureStorageKey.email);
if (url != null) {
_urlController.text = url;
}
if (username != null) {
_usernameController.text = username;
}
}
Future<bool> _isJwtValid(String jwt) async {
try {
bool expired = JwtDecoder.isExpired(jwt);
return !expired;
} on FormatException {
return false;
}
}
Future<void> _login(
String serverUrl, String username, String password) async {
setState(() {
_error = null;
});
final apiClient = ApiClient(baseUrl: serverUrl);
try {
await apiClient.login(username, password);
await _storageHelper.writeSecure(SecureStorageKey.url, serverUrl);
await _storageHelper.writeSecure(SecureStorageKey.jwt, apiClient.token!);
await _storageHelper.writeSecure(SecureStorageKey.email, username);
await _navigateToMainPage();
} catch (e) {
setState(() {
_error = "Login failed.\n$e";
});
}
}
Future<void> _navigateToMainPage() async {
final config = await _storageHelper.readConfig();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => MyHomePage(config: config)),
);
}
String? validateNotEmpty(String? content) {
if (content == null || content.isEmpty) {
return "Do not leave this field empty";
}
return null;
}
void handleLoginPressed() async {
if (loggingIn) return;
loggingIn = true;
if (_formKey.currentState!.validate()) {
await _login(
_urlController.text,
_usernameController.text,
_passwordController.text,
);
}
loggingIn = false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _urlController,
validator: validateNotEmpty,
decoration: InputDecoration(labelText: 'Url'),
textInputAction: TextInputAction.next,
),
TextFormField(
controller: _usernameController,
validator: validateNotEmpty,
decoration: InputDecoration(labelText: 'Username'),
textInputAction: TextInputAction.next,
),
TextFormField(
controller: _passwordController,
validator: validateNotEmpty,
// focusNode: _passwordFocusNode,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
textInputAction: TextInputAction
.next, // with submit or go, onFieldSubmitted is not called
onFieldSubmitted: (_) => handleLoginPressed(),
),
// ),
SizedBox(height: 5),
ElevatedButton(
onPressed: loggingIn ? null : handleLoginPressed,
child: Text('Login'),
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(_error!, style: TextStyle(color: Colors.red)),
),
],
),
),
),
);
}
}