import 'package:flutter/material.dart'; import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/snackbar.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; }); final scaffMgr = ScaffoldMessenger.of(context); // verify that both passwords are the same if (widget.mode != Mode.signin) { if (_ctrPassword.text != _ctrPasswordRpt.text) { setState(() { showSpinner = false; }); showSimpleSnackbar(scaffMgr, text: 'Passwords do not match', action: 'Dismiss'); _ctrPasswordRpt.clear(); return; } } // password has to be at least 6 characters long if (_ctrPassword.text.length < 6) { setState(() { showSpinner = false; }); showSimpleSnackbar(scaffMgr, text: 'Password has to be at least 6 characters longs', action: 'Dismiss'); _ctrPasswordRpt.clear(); return; } // 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; }); showSimpleSnackbar(scaffMgr, text: 'Unable to find valid outbag server on ${_ctrServer.text}', action: 'Dismiss'); return; } // hash password var bytes = utf8.encode(_ctrPassword.text); final password = sha256.convert(bytes).toString(); doNetworkRequest( scaffMgr, needUser: false, req: (_) { if (widget.mode == Mode.signin) { return postUnauthorized( target: server, path: 'signin', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password }); } else if (widget.mode == Mode.signup) { return postUnauthorized( target: server, path: 'signup', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password }); } else { // signup OTA return postUnauthorized( target: server, path: 'signupOTA', body: { 'name': _ctrUsername.text, 'server': server.tag, 'accountKey': password, 'OTA': _ctrOTA.text }); } }, onOK: (body) async { // authorize user await User( username: _ctrUsername.text, password: password, server: server) .toDisk(); }, after: () { setState(() { showSpinner = false; }); } ); }, label: Text(modeName), icon: const Icon(Icons.check), tooltip: modeDescription, ), ); } }