import 'package:flutter/material.dart'; import 'package:outbag_app/backend/crypto.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:flutter_gen/gen_l10n/app_localizations.dart'; import '../backend/resolve_url.dart'; enum Mode { signin, signup, signupOTA, } class AuthPage extends StatefulWidget { final Mode mode; Function refresh; AuthPage({super.key, required this.mode, required this.refresh}); @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 = AppLocalizations.of(context)!.signIn; if (widget.mode != Mode.signin) { modeName = AppLocalizations.of(context)!.signUp; } String modeDescription = AppLocalizations.of(context)!.logIntoAccount; if (widget.mode != Mode.signin) { modeDescription = AppLocalizations.of(context)!.createNewAccount; } 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(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge), ]))) : Scaffold( appBar: AppBar( title: Text(modeName), ), body: Center( 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: InputDecoration( prefixIcon: const Icon(Icons.dns), labelText: AppLocalizations.of(context)! .inputServerLabel, hintText: AppLocalizations.of(context)!.inputServerHint, helperText: AppLocalizations.of(context)!.inputServerHelp, border: const OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrUsername, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( prefixIcon: const Icon(Icons.person), labelText: AppLocalizations.of(context)! .inputUsernameLabel, hintText: AppLocalizations.of(context)! .inputUsernameHint, helperText: AppLocalizations.of(context)! .inputUsernameHelp, border: const OutlineInputBorder(), ), ), ), Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrPassword, keyboardType: TextInputType.visiblePassword, obscureText: true, decoration: InputDecoration( prefixIcon: const Icon(Icons.lock), labelText: AppLocalizations.of(context)! .inputPasswordLabel, hintText: AppLocalizations.of(context)! .inputPasswordHint, helperText: AppLocalizations.of(context)! .inputPasswordHelp, border: const OutlineInputBorder(), ), ), ), // ONLY SIGNUP ...((widget.mode != Mode.signin) ? [ Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrPasswordRpt, keyboardType: TextInputType.visiblePassword, obscureText: true, decoration: InputDecoration( prefixIcon: const Icon(Icons.lock), labelText: AppLocalizations.of(context)! .inputPasswordRepeatLabel, hintText: AppLocalizations.of(context)! .inputPasswordRepeatHint, helperText: AppLocalizations.of(context)! .inputPasswordRepeatHelp, border: const OutlineInputBorder(), ), ), ) ] : []), // ONLY SIGNUP OTA ...((widget.mode == Mode.signupOTA) ? [ Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _ctrOTA, keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( prefixIcon: const Icon(Icons.key), labelText: AppLocalizations.of(context)! .inputOTALabel, hintText: AppLocalizations.of(context)! .inputOTAHint, helperText: AppLocalizations.of(context)! .inputOTAHelp, border: const 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: AppLocalizations.of(context)! .errorPasswordsDoNotMatch, action: AppLocalizations.of(context)!.dismiss); _ctrPasswordRpt.clear(); return; } } // password has to be at least 6 characters long if (_ctrPassword.text.length < 6) { setState(() { showSpinner = false; }); showSimpleSnackbar(scaffMgr, text: AppLocalizations.of(context)!.errorPasswordLength, action: AppLocalizations.of(context)!.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: AppLocalizations.of(context)! .errorInvalidServer(_ctrServer.text), action: AppLocalizations.of(context)!.dismiss); return; } // hash password final password = hashPassword(_ctrPassword.text); doNetworkRequest(scaffMgr, 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(); widget.refresh(); }, after: () { setState(() { showSpinner = false; }); }); }, label: Text(modeName), icon: const Icon(Icons.check), tooltip: modeDescription, ), ); } }