From 4c06208e82df48946551a6f8c6e8b0e6165856cf Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 17 Mar 2023 21:07:05 +0100 Subject: [PATCH] Basic auth screen --- lib/screens/auth.dart | 345 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 lib/screens/auth.dart diff --git a/lib/screens/auth.dart b/lib/screens/auth.dart new file mode 100644 index 0000000..ce84e3a --- /dev/null +++ b/lib/screens/auth.dart @@ -0,0 +1,345 @@ +import 'package:flutter/material.dart'; +import 'package:outbag_app/backend/request.dart'; +import 'package:outbag_app/backend/storage.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 + LoginDetails( + 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, + ), + ); + } +}