import 'package:flutter/material.dart'; import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:routemaster/routemaster.dart'; import '../backend/resolve_url.dart'; import '../backend/errors.dart'; import 'package:crypto/crypto.dart'; import 'dart:convert'; enum Mode { signin, signup, signupOTA, } class AuthPage extends StatefulWidget { final Mode mode; const AuthPage({super.key, this.mode = Mode.signin}); @override State createState() => _AuthPageState(); } class _AuthPageState extends State { final TextEditingController _ctrServer = TextEditingController(); final TextEditingController _ctrUsername = TextEditingController(); final TextEditingController _ctrPassword = TextEditingController(); final TextEditingController _ctrPasswordRpt = TextEditingController(); final TextEditingController _ctrOTA = TextEditingController(); bool showSpinner = false; @override Widget build(BuildContext context) { String modeName = "Sign In"; if (widget.mode != Mode.signin) { modeName = "Sign Up"; } String modeDescription = "Log into account"; if (widget.mode != Mode.signin) { modeDescription = "Create new account"; } final textTheme = Theme.of(context) .textTheme .apply(displayColor: Theme.of(context).colorScheme.onSurface); return showSpinner ? Scaffold( body: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), Text('Loading', style: textTheme.titleLarge), ]))) : Scaffold( appBar: AppBar( title: Text(modeName), leading: IconButton( onPressed: () { // go back Routemaster.of(context).history.back(); }, icon: const Icon(Icons.arrow_back), tooltip: "Go back", ), ), body: Center( child: Flexible( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrServer, keyboardType: TextInputType.url, decoration: const InputDecoration( prefixIcon: Icon(Icons.dns), labelText: 'Server', hintText: 'Your homeserver url', helperText: 'Your data will be stored on your homeserver', border: OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrUsername, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( prefixIcon: Icon(Icons.person), labelText: 'Username', hintText: 'Your username', helperText: 'your username and server tag allow others to identify you', border: OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrPassword, keyboardType: TextInputType.visiblePassword, obscureText: true, decoration: const InputDecoration( prefixIcon: Icon(Icons.lock), labelText: 'Password', hintText: 'Your password', helperText: 'Password have to be at least six characters long', border: OutlineInputBorder(), ), ), ), // ONLY SIGNUP ...((widget.mode != Mode.signin) ? [ Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrPasswordRpt, keyboardType: TextInputType.visiblePassword, obscureText: true, decoration: const InputDecoration( prefixIcon: Icon(Icons.lock), labelText: 'Repeat Password', hintText: 'Type your password again', helperText: 'Make sure to type the correct password', border: OutlineInputBorder(), ), ), ) ] : []), // ONLY SIGNUP OTA ...((widget.mode == Mode.signupOTA) ? [ Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrOTA, keyboardType: TextInputType.visiblePassword, decoration: const InputDecoration( prefixIcon: Icon(Icons.key), labelText: 'OTA', hintText: 'One-Time-Authorization token', helperText: 'This token might be required if the server is rate limited', border: OutlineInputBorder(), ), ), ) ] : []), ], )))), floatingActionButton: FloatingActionButton.extended( onPressed: () async { setState(() { showSpinner = true; }); // verify that both passwords are the same if (widget.mode != Mode.signin) { if (_ctrPassword.text != _ctrPasswordRpt.text) { setState(() { showSpinner = false; }); final snackBar = SnackBar( behavior: SnackBarBehavior.floating, content: const Text('Passwords do not match'), action: SnackBarAction( label: 'Dismiss', onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); }, ), ); ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar(snackBar); _ctrPasswordRpt.clear(); return; } } // password has to be at least 6 characters long if (_ctrPassword.text.length < 6) { setState(() { showSpinner = false; }); final snackBar = SnackBar( behavior: SnackBarBehavior.floating, content: const Text( 'Password has to be at least 6 characters long'), action: SnackBarAction( label: 'Dismiss', onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); }, ), ); ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar(snackBar); _ctrPasswordRpt.clear(); return; } final scaffMgr = ScaffoldMessenger.of(context); // TODO: resolve homeserver url OutbagServer server; try { server = await getOutbagServerUrl(_ctrServer.text); } catch (e) { // unable to find outbag server // at given server address // stop authentification setState(() { showSpinner = false; }); final snackBar = SnackBar( behavior: SnackBarBehavior.floating, content: Text( 'Unable to find valid outbag server on ${_ctrServer.text}'), action: SnackBarAction( label: 'Dismiss', onPressed: () { scaffMgr.hideCurrentSnackBar(); }, ), ); scaffMgr.hideCurrentSnackBar(); scaffMgr.showSnackBar(snackBar); return; } var bytes = utf8.encode(_ctrPassword.text); final password = sha256.convert(bytes).toString(); try { Response resp; // validate account if (widget.mode == Mode.signin) { resp = await postUnauthorized( target: server, path: 'signin', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password }); } else if (widget.mode == Mode.signup) { resp = await postUnauthorized( target: server, path: 'signup', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password }); } else { // signup OTA resp = await postUnauthorized( target: server, path: 'signupOTA', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password, 'OTA': _ctrOTA.text }); } if (resp.res == Result.err) { // error final snackBar = SnackBar( behavior: SnackBarBehavior.floating, content: Text(errorAsString(resp.body)), action: SnackBarAction( label: 'Dismiss', onPressed: () { scaffMgr.hideCurrentSnackBar(); }, ), ); scaffMgr.hideCurrentSnackBar(); scaffMgr.showSnackBar(snackBar); } else { // authorize user await User( username: _ctrUsername.text, password: password, server: server) .toDisk(); } } catch (_) { final snackBar = SnackBar( behavior: SnackBarBehavior.floating, content: const Text('Network error'), action: SnackBarAction( label: 'Dismiss', onPressed: () { scaffMgr.hideCurrentSnackBar(); }, ), ); scaffMgr.hideCurrentSnackBar(); scaffMgr.showSnackBar(snackBar); } setState(() { showSpinner = false; }); }, label: Text(modeName), icon: const Icon(Icons.check), tooltip: modeDescription, ), ); } }