added guardian login with views for their assigned childs
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../session/session.dart';
|
||||
import '../../../session/session_manager.dart';
|
||||
import '../queries/auth_login/auth_login.dart';
|
||||
import 'device_token_name.dart';
|
||||
import 'token_storage.dart';
|
||||
|
||||
/// Adds the bearer token to outgoing Marianum-Connect requests and, on 401,
|
||||
/// re-logs in once with the credentials in [AccountData] before retrying.
|
||||
/// renews the token once before retrying. Only password accounts can renew
|
||||
/// silently; passwordless accounts surface the 401.
|
||||
class MarianumConnectAuthInterceptor extends Interceptor {
|
||||
static const _retriedKey = 'mc_auth_retried';
|
||||
|
||||
@@ -64,6 +66,9 @@ class MarianumConnectAuthInterceptor extends Interceptor {
|
||||
}
|
||||
final refreshed = await _attemptReLogin();
|
||||
if (!refreshed) {
|
||||
if (SessionManager().current is GuardianSession) {
|
||||
SessionManager().reportUnauthorized();
|
||||
}
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
@@ -87,11 +92,12 @@ class MarianumConnectAuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
Future<bool> _performReLogin() async {
|
||||
if (!AccountData().isPopulated()) return false;
|
||||
final session = SessionManager().current;
|
||||
if (session is! CredentialSession) return false;
|
||||
try {
|
||||
await _loginClient.run(
|
||||
username: AccountData().getUsername(),
|
||||
password: AccountData().getPassword(),
|
||||
username: session.username,
|
||||
password: session.password,
|
||||
tokenName: await DeviceTokenName.resolve(),
|
||||
);
|
||||
return true;
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../session/session.dart';
|
||||
import '../../../session/session_lifecycle.dart';
|
||||
import '../../../session/session_manager.dart';
|
||||
import '../../errors/auth_exception.dart';
|
||||
import '../queries/auth_logout/auth_logout.dart';
|
||||
import '../queries/auth_me/auth_me.dart';
|
||||
import '../queries/auth_verify/auth_verify.dart';
|
||||
import 'token_storage.dart';
|
||||
|
||||
/// Background credential probe — a server-side password rotation forces a
|
||||
/// re-login on the next cold start even when the bearer token would still
|
||||
/// be accepted.
|
||||
/// Credential probe. For password accounts a server-side password rotation
|
||||
/// forces a re-login on the next cold start even when the bearer token would
|
||||
/// still be accepted; for guardians it confirms a rejected token before the
|
||||
/// session is dropped.
|
||||
class SessionValidator {
|
||||
static Future<void> probeStored({
|
||||
required Future<void> Function() onInvalidated,
|
||||
}) async {
|
||||
if (!AccountData().isPopulated()) return;
|
||||
// AuthVerify uses its own dio (bypassing the demo interceptor), so a demo
|
||||
// session must be skipped here or its missing token would 401 into a logout.
|
||||
if (AccountData().isDemo) return;
|
||||
final username = AccountData().getUsername();
|
||||
final password = AccountData().getPassword();
|
||||
final session = SessionManager().current;
|
||||
// The probes use their own dio (bypassing the demo interceptor), so a demo
|
||||
// session must be skipped or its missing token would 401 into a logout.
|
||||
if (session == null || session.isDemo) return;
|
||||
try {
|
||||
await AuthVerify().run(username: username, password: password);
|
||||
switch (session) {
|
||||
case CredentialSession(:final username, :final password):
|
||||
await AuthVerify().run(username: username, password: password);
|
||||
case GuardianSession():
|
||||
await AuthMe().run();
|
||||
}
|
||||
} on AuthException catch (e) {
|
||||
if (e.statusCode != 401) return;
|
||||
log('MC: stored credentials rejected — forcing re-login');
|
||||
await AuthLogout().run();
|
||||
await const MarianumConnectTokenStorage().clear();
|
||||
await AccountData().removeData();
|
||||
log('MC: stored session rejected — forcing re-login');
|
||||
await SessionLifecycle.signOut();
|
||||
await onInvalidated();
|
||||
} catch (e) {
|
||||
log('MC: background credential check failed (transient): $e');
|
||||
log('MC: background session check failed (transient): $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../../errors/auth_exception.dart';
|
||||
|
||||
/// `first_unlock` accessibility so the token can be read during background
|
||||
/// requests (telemetry heartbeat, push-triggered syncs) after the first device
|
||||
/// unlock following a reboot. The keychain default (`whenUnlocked`) throws
|
||||
@@ -9,7 +12,7 @@ const IOSOptions _mcIosOptions = IOSOptions(
|
||||
);
|
||||
|
||||
/// Persists the Marianum-Connect bearer token in the platform keystore. Kept
|
||||
/// separate from `AccountData` because the username/password live on (Nextcloud
|
||||
/// separate from `SessionManager` because the username/password live on (Nextcloud
|
||||
/// + MHSL still need them) while the MC token is short-lived and per-endpoint.
|
||||
class MarianumConnectTokenStorage {
|
||||
static const _tokenKey = 'mc_bearer_token';
|
||||
@@ -24,6 +27,18 @@ class MarianumConnectTokenStorage {
|
||||
|
||||
Future<String?> readToken() => _storage.read(key: _tokenKey);
|
||||
|
||||
/// Request options carrying the stored token, for probes that bypass the
|
||||
/// auth interceptor. Throws [AuthException] when no token is stored.
|
||||
Future<Options> requireBearerOptions(String caller) async {
|
||||
final token = await readToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
throw AuthException.unauthorized(
|
||||
technicalDetails: '$caller: no bearer token in storage',
|
||||
);
|
||||
}
|
||||
return Options(headers: {'Authorization': 'Bearer $token'});
|
||||
}
|
||||
|
||||
Future<String?> readTokenId() => _storage.read(key: _tokenIdKey);
|
||||
|
||||
Future<DateTime?> readExpiresAt() async {
|
||||
|
||||
Reference in New Issue
Block a user