fixed guardian login edge cases and misleading placeholders on failed loads

This commit is contained in:
2026-09-20 19:03:45 +02:00
parent 2423c1a75e
commit 630497abdd
23 changed files with 299 additions and 72 deletions
@@ -29,7 +29,15 @@ class SessionValidator {
} on AuthException catch (e) {
if (e.statusCode != 401) return;
log('MC: stored session rejected — forcing re-login');
await SessionLifecycle.signOut();
await SessionLifecycle.signOut(
notice: switch (session) {
CredentialSession() =>
'Deine Zugangsdaten wurden vom Server abgelehnt. Vermutlich '
'wurde dein Passwort geändert. Bitte melde dich erneut an.',
GuardianSession() =>
'Deine Anmeldung ist abgelaufen. Bitte melde dich erneut an.',
},
);
await onInvalidated();
} catch (e) {
log('MC: background session check failed (transient): $e');
@@ -1,3 +1,4 @@
import '../../../errors/app_exception.dart';
import '../../marianumconnect_query.dart';
import 'absence_prefill_response.dart';
@@ -13,3 +14,17 @@ class AbsencePrefill extends MarianumConnectQuery {
queryParameters: {'childId': ?childId},
);
}
/// The prefill of a guardian's report is missing a field the form cannot ask
/// for: name and class come from the child and are not editable there, so an
/// incomplete prefill would leave a locked, unsubmittable form behind.
class AbsencePrefillIncompleteException extends AppException {
const AbsencePrefillIncompleteException({super.technicalDetails})
: super(
userMessage:
'Für dieses Kind sind in der Schulverwaltung noch nicht alle '
'Angaben hinterlegt, die für eine Abwesenheitsmeldung nötig sind. '
'Bitte wende dich an das Sekretariat.',
allowRetry: false,
);
}
@@ -30,8 +30,10 @@ class AuthGuardianRequestResponse {
}
/// Starts a passwordless guardian login: the server mails a code and a link.
/// The answer is identical for unknown addresses, so it reveals nothing about
/// which e-mails are registered.
/// An unknown address is reported as such (`email_not_registered`) instead of
/// being answered like a known one — a deliberate trade: without it guardians
/// wait for a mail that never arrives. Registered addresses are therefore
/// enumerable; keep that in mind when changing the server contract.
class AuthGuardianRequest extends MarianumConnectQuery {
AuthGuardianRequest({Dio? dio})
: super(dio: dio ?? MarianumConnectApi.plainDio());
@@ -52,7 +54,7 @@ class AuthGuardianRequest extends MarianumConnectQuery {
);
return AuthGuardianRequestResponse.fromJson(response.data!);
} on DioException catch (e) {
throw GuardianLoginException.fromDio(e);
throw GuardianLoginException.fromDio(e, verifying: false);
}
}
}
@@ -31,14 +31,17 @@ class GuardianLoginException extends AppException {
/// Maps a failed guardian auth call. Only 4xx answers that carry a guardian
/// login reason become [GuardianLoginException]; everything else keeps the
/// generic MarianumConnect mapping (network, 5xx, …).
static AppException fromDio(DioException e) {
///
/// [verifying] false marks the e-mail step, where no code exists yet: a bare
/// 401 (reverse proxy, gateway) must not be reported as a wrong code there.
static AppException fromDio(DioException e, {bool verifying = true}) {
final response = e.response;
final status = response?.statusCode;
if (status == null || status < 400 || status >= 500) {
return mapMarianumConnectError(e);
}
final (code, attemptsLeft) = _parseBody(response!.data);
final error = _errorFor(code, status);
final error = _errorFor(code, status, verifying: verifying);
if (error == null) return mapMarianumConnectError(e);
return GuardianLoginException(
error,
@@ -89,8 +92,11 @@ class GuardianLoginException extends AppException {
return (marianumConnectErrorCode(data), attempts is int ? attempts : null);
}
static GuardianLoginError? _errorFor(String? code, int status) =>
switch (code) {
static GuardianLoginError? _errorFor(
String? code,
int status, {
required bool verifying,
}) => switch (code) {
'invalid_request' => GuardianLoginError.invalidRequest,
'email_not_registered' => GuardianLoginError.emailNotRegistered,
'account_disabled' => GuardianLoginError.accountDisabled,
@@ -100,7 +106,7 @@ class GuardianLoginException extends AppException {
'request_expired' => GuardianLoginError.requestExpired,
'too_many_attempts' => GuardianLoginError.tooManyAttempts,
_ => switch (status) {
401 => GuardianLoginError.invalidCode,
401 when verifying => GuardianLoginError.invalidCode,
// The server names an unknown address explicitly
// (`email_not_registered`); a bare 404/405 means the endpoint itself
// is missing, i.e. a server version without guardian login.