4bbdcaad4d
NOTE: Might be a good idea to move the title down, if no description is available. If ListTile does not support such feature, maybe using an alternative might be good idea
344 lines
14 KiB
Dart
344 lines
14 KiB
Dart
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<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 = "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,
|
|
),
|
|
);
|
|
}
|
|
}
|