added guardian login with views for their assigned childs
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../api/demo/demo_mode.dart';
|
||||
import '../../api/errors/error_mapper.dart';
|
||||
import '../../api/marianumconnect/auth/device_token_name.dart';
|
||||
import '../../api/marianumconnect/queries/auth_guardian/auth_guardian_request.dart';
|
||||
import '../../api/marianumconnect/queries/auth_guardian/auth_guardian_verify.dart';
|
||||
import '../../api/marianumconnect/queries/auth_guardian/guardian_login_exception.dart';
|
||||
import '../../auth_link/device_binding.dart';
|
||||
import '../../auth_link/guardian_login_link.dart';
|
||||
import '../../auth_link/pending_guardian_request.dart';
|
||||
import '../../session/session.dart';
|
||||
import '../../session/session_manager.dart';
|
||||
import '../../widget_data/widget_sync.dart';
|
||||
|
||||
enum GuardianLoginStep { enterEmail, enterCode }
|
||||
|
||||
/// Drives the passwordless guardian login: request a mail, then finish with
|
||||
/// the mailed code or link. The running request is persisted so the flow
|
||||
/// survives the app being killed while the user reads the mail.
|
||||
class GuardianLoginController extends ChangeNotifier {
|
||||
final AuthGuardianRequest _request;
|
||||
final AuthGuardianVerify _verify;
|
||||
final PendingGuardianRequestStore _store;
|
||||
final Future<void> Function(Session session) _signIn;
|
||||
final Future<String> Function() _tokenName;
|
||||
final DateTime Function() _now;
|
||||
|
||||
GuardianLoginController({
|
||||
AuthGuardianRequest? request,
|
||||
AuthGuardianVerify? verify,
|
||||
PendingGuardianRequestStore store = const PendingGuardianRequestStore(),
|
||||
Future<void> Function(Session session)? signIn,
|
||||
Future<String> Function()? tokenName,
|
||||
DateTime Function()? now,
|
||||
}) : _request = request ?? AuthGuardianRequest(),
|
||||
_verify = verify ?? AuthGuardianVerify(),
|
||||
_store = store,
|
||||
_signIn = signIn ?? _defaultSignIn,
|
||||
_tokenName = tokenName ?? DeviceTokenName.resolve,
|
||||
_now = now ?? DateTime.now;
|
||||
|
||||
GuardianLoginStep _step = GuardianLoginStep.enterEmail;
|
||||
PendingGuardianRequest? _pending;
|
||||
bool _loading = false;
|
||||
String? _errorMessage;
|
||||
String? _errorDetails;
|
||||
|
||||
GuardianLoginStep get step => _step;
|
||||
PendingGuardianRequest? get pending => _pending;
|
||||
bool get loading => _loading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
String? get errorDetails => _errorDetails;
|
||||
|
||||
bool canResend() {
|
||||
final pending = _pending;
|
||||
return pending != null && !_now().isBefore(pending.resendAvailableAt);
|
||||
}
|
||||
|
||||
/// Picks up a request started before the app was closed.
|
||||
Future<void> restore() async {
|
||||
final stored = await _store.read();
|
||||
if (stored == null) return;
|
||||
if (stored.isExpired(_now())) {
|
||||
await _store.clear();
|
||||
return;
|
||||
}
|
||||
_pending = stored;
|
||||
_step = GuardianLoginStep.enterCode;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Sends the login mail. Returns true when the user is already signed in
|
||||
/// (demo address), false when the code step follows or the request failed.
|
||||
Future<bool> requestCode(String email) async {
|
||||
final normalized = email.trim().toLowerCase();
|
||||
if (DemoMode.matchesGuardian(normalized)) {
|
||||
await _signIn(GuardianSession(email: normalized, isDemo: true));
|
||||
return true;
|
||||
}
|
||||
await _run(() async {
|
||||
final secret = DeviceBinding.generateSecret();
|
||||
final response = await _request.run(
|
||||
email: normalized,
|
||||
deviceChallenge: DeviceBinding.challengeFor(secret),
|
||||
tokenName: await _tokenName(),
|
||||
);
|
||||
final pending = PendingGuardianRequest(
|
||||
requestId: response.requestId,
|
||||
email: normalized,
|
||||
deviceSecret: secret,
|
||||
expiresAt: response.expiresAt,
|
||||
resendAvailableAt: response.resendAvailableAt,
|
||||
codeLength: response.codeLength,
|
||||
);
|
||||
await _store.write(pending);
|
||||
_pending = pending;
|
||||
_step = GuardianLoginStep.enterCode;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> resend() async {
|
||||
final pending = _pending;
|
||||
if (pending == null || !canResend()) return;
|
||||
await requestCode(pending.email);
|
||||
}
|
||||
|
||||
/// Mail clients and autofill may insert spaces into the code.
|
||||
static String normalizeCode(String code) =>
|
||||
code.replaceAll(RegExp(r'\s'), '');
|
||||
|
||||
Future<bool> submitCode(String code) => _complete(code: normalizeCode(code));
|
||||
|
||||
/// Completes the login from a mail link. A link belonging to another
|
||||
/// request (other device, or an older mail) cannot be verified here.
|
||||
Future<bool> submitLink(GuardianLoginLink link) {
|
||||
if (_pending?.requestId != link.requestId) {
|
||||
_errorMessage = GuardianLoginException.messageFor(
|
||||
GuardianLoginError.deviceMismatch,
|
||||
);
|
||||
_errorDetails = null;
|
||||
notifyListeners();
|
||||
return Future.value(false);
|
||||
}
|
||||
return _complete(linkToken: link.linkToken);
|
||||
}
|
||||
|
||||
/// Abandons the running request, e.g. to correct a mistyped address.
|
||||
Future<void> changeEmail() async {
|
||||
await _store.clear();
|
||||
_pending = null;
|
||||
_step = GuardianLoginStep.enterEmail;
|
||||
_errorMessage = null;
|
||||
_errorDetails = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> _complete({String? code, String? linkToken}) async {
|
||||
final pending = _pending;
|
||||
if (pending == null) return false;
|
||||
var signedIn = false;
|
||||
await _run(() async {
|
||||
await _verify.run(
|
||||
requestId: pending.requestId,
|
||||
deviceVerifier: pending.deviceSecret,
|
||||
tokenName: await _tokenName(),
|
||||
code: code,
|
||||
linkToken: linkToken,
|
||||
);
|
||||
await _store.clear();
|
||||
await _signIn(GuardianSession(email: pending.email));
|
||||
signedIn = true;
|
||||
});
|
||||
return signedIn;
|
||||
}
|
||||
|
||||
Future<void> _run(Future<void> Function() body) async {
|
||||
if (_loading) return;
|
||||
_loading = true;
|
||||
_errorMessage = null;
|
||||
_errorDetails = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
await body();
|
||||
} on GuardianLoginException catch (e) {
|
||||
_errorMessage = e.userMessage;
|
||||
_errorDetails = e.technicalDetails;
|
||||
// These end the request for good; only a new mail helps.
|
||||
if (e.error
|
||||
case GuardianLoginError.requestExpired ||
|
||||
GuardianLoginError.requestConsumed ||
|
||||
GuardianLoginError.tooManyAttempts) {
|
||||
await _store.clear();
|
||||
_pending = null;
|
||||
_step = GuardianLoginStep.enterEmail;
|
||||
}
|
||||
} catch (e) {
|
||||
log('Guardian login failed: $e');
|
||||
_errorMessage = errorToUserMessage(e);
|
||||
_errorDetails = errorToTechnicalDetails(e);
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _defaultSignIn(Session session) async {
|
||||
// Drop any widget snapshot of a previous account before the new one loads.
|
||||
await WidgetSync.clear();
|
||||
await WidgetSync.triggerUpdate();
|
||||
await SessionManager().signIn(session);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user