import "package:flutter/foundation.dart"; import "api.dart"; import "i18n.dart"; import "models.dart"; import "servers.dart"; /// Session state for whichever server is active. The tokens themselves are held /// per server by [serverRegistry] — each one is a separate PocketBase, so a /// session cannot be shared — and this service mirrors the active one, so every /// screen goes on reading `authService.user` without knowing that more than one /// server exists. class AuthService extends ChangeNotifier { final ApiClient api; bool ready = false; /// In-memory (never persisted) app-lock flag. When biometric login is enabled /// the app starts/returns locked: the token is still valid but the UI hides /// behind a biometric unlock instead of jumping straight to the dashboard. bool locked = false; AuthService(this.api) { api.onUnauthorized = _onUnauthorized; // Switching servers swaps the whole session: a different PocketBase, a // different user record. Everything watching this service rebuilds on it. serverRegistry.addListener(notifyListeners); } /// The user of the active server's session, or null when it has none. AuthUser? get user => serverRegistry.activeSession?.user; bool get isAuthenticated => serverRegistry.activeToken != null; void lock() { if (!locked && isAuthenticated) { locked = true; notifyListeners(); } } void unlock() { if (locked) { locked = false; notifyListeners(); } } /// Sessions are read by [ServerRegistry.load]; this only flips the flag that /// tells the app boot is done. Future loadFromStorage() async { ready = true; notifyListeners(); } /// Logs into one server and makes it active. The login screen calls it for /// whichever server is active; the server sheet calls it for one that isn't /// yet, which is why the credentials are checked against that server's own /// base before anything switches. Future connect(String serverId, String email, String password) async { final server = serverRegistry.byId(serverId); if (server == null) throw ApiException(404, t("servers.unknown")); final (token, u) = await api.loginAt(serverRegistry.baseFor(server), email, password); locked = false; serverRegistry.setSession(serverId, token, u); serverRegistry.setActive(serverId); notifyListeners(); } Future login(String email, String password) => connect(serverRegistry.activeId, email, password); /// Signs out of one server without leaving the app — the server sheet's own /// action. Dropping the active one falls back to home, the same way an expired /// token does. void disconnect(String serverId) { serverRegistry.clearSession(serverId); if (serverRegistry.activeId == serverId && serverId != kHomeServerId) { serverRegistry.setActive(kHomeServerId); } notifyListeners(); } /// Replaces the cached user of the active server's session — the settings /// screen renaming the account, say. The token it was minted with stands. void adoptUser(AuthUser updated) { final session = serverRegistry.activeSession; if (session == null) return; serverRegistry.setSession(serverRegistry.activeId, session.token, updated); notifyListeners(); } /// Adopts the role from a freshly fetched profile. A session's role can change /// under it — creating an organization promotes the creator to that org's admin /// — and the nav gates the Users tab on the cached copy, so it has to catch up /// without requiring a re-login. A no-op when the role is unchanged. Future adoptRole(UserProfile profile) async { final u = user; if (u == null || profile.role == u.role) return; adoptUser(AuthUser(id: u.id, email: u.email, name: u.name, role: profile.role)); } /// Signs out everywhere. Leaving the app means leaving every server you /// reached from it — a live token left behind on a shared phone would be worse /// than the inconvenience of logging back in. Future logout() async { locked = false; serverRegistry.clearAllSessions(); serverRegistry.setActive(kHomeServerId); notifyListeners(); } /// A server rejected the token it was given. Only that server's session ends: /// a remote one timing out shouldn't tip you out of the app, so fall back to /// home while it is still connected, and land on the login screen only when /// there is nothing left to fall back to. void _onUnauthorized(String serverId) { serverRegistry.clearSession(serverId); if (serverRegistry.activeId == serverId && serverId != kHomeServerId && serverRegistry.isConnected(kHomeServerId)) { serverRegistry.setActive(kHomeServerId); } if (!isAuthenticated) locked = false; notifyListeners(); } }