8fffafde47
Translations are provided in *.arb* format. Some keys have descriptions (indicated by leading @-symbol). Descriptions should not be copied into the translation itself. Currently only English is supported (app_en.arb), but German is planned. Apparently weblate merged .arb support at some time, so it would be nice to enable community translations at some point.
263 lines
11 KiB
Dart
263 lines
11 KiB
Dart
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;
|
|
const AuthPage({super.key, this.mode = Mode.signin});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _AuthPageState();
|
|
}
|
|
|
|
class _AuthPageState extends State<AuthPage> {
|
|
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();
|
|
}, after: () {
|
|
setState(() {
|
|
showSpinner = false;
|
|
});
|
|
});
|
|
},
|
|
label: Text(modeName),
|
|
icon: const Icon(Icons.check),
|
|
tooltip: modeDescription,
|
|
),
|
|
);
|
|
}
|
|
}
|