import 'package:flutter/foundation.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/token_storage_service.dart'; class AuthService extends ChangeNotifier { final ApiService api; bool _restoring = false; final TokenStorageService _tokenStorage = TokenStorageService(); AuthService({required this.api}) { api.setTokenProvider(() => token); api.setUnauthorizedHandler(handleTokenExpired); } AuthenticatedUserData? _user; bool get isLoggedIn => _user != null; String? get token => _user?.accessToken; String? get userId => _user?.userId; String? get username => _user?.username; String? get fullName => _user?.fullName; bool get isElevated => _user?.isElevated ?? false; bool get isAdmin => isElevated; // alias for old name bool get isDisabled => _user?.disabled ?? false; void setLoginData({ required String userId, required String username, required String fullName, required String accessToken, required String email, bool isElevated = false, bool isDisabled = false, }) { _user = AuthenticatedUserData( userId: userId, username: username, fullName: fullName, accessToken: accessToken, email: email, isElevated: isElevated, disabled: isDisabled, ); _persistToken(accessToken); notifyListeners(); } Future login(String username, String password) async { final formData = { 'grant_type': 'password', 'username': username, 'password': password, 'scope': '', 'client_id': 'string', 'client_secret': 'string', }; // 1. Get token final tokenResponse = await api.postForm('/token', formData); final accessToken = tokenResponse['access_token']; // 2. Get user details final userResponse = await api.get( '/users/me', headers: { 'Authorization': 'Bearer $accessToken', 'accept': 'application/json', }, ); // 3. Populate state setLoginData( userId: userResponse['user_id'], username: userResponse['username'], fullName: userResponse['full_name'], accessToken: accessToken, email: userResponse['email'], isElevated: _parseIsElevated(userResponse), isDisabled: _parseIsDisabled(userResponse), ); } Future tryRestoreSession() async { if (_restoring || _user != null) return; _restoring = true; try { // read token from secure storage (with fallback) final token = await _tokenStorage.getToken(); if (token == null || token.isEmpty) return; final userResponse = await api.get( '/users/me', headers: { 'Authorization': 'Bearer $token', 'accept': 'application/json', }, ); setLoginData( userId: userResponse['user_id'], username: userResponse['username'], fullName: userResponse['full_name'], accessToken: token, email: userResponse['email'], isElevated: _parseIsElevated(userResponse), isDisabled: _parseIsDisabled(userResponse), ); } catch (_) { await _clearToken(); } finally { _restoring = false; } } Future validateStoredToken() async { final token = await _tokenStorage.getToken(); if (token == null || token.isEmpty) return false; try { await api.get( '/validate', headers: { 'Authorization': 'Bearer $token', 'accept': 'application/json', }, ); return true; } catch (_) { await _clearToken(); return false; } } Future _persistToken(String token) async { await _tokenStorage.setToken(token); } Future _clearToken() async { await _tokenStorage.clearToken(); } Future register({ required String username, required String email, required String fullName, required String password, String inviteCode = '', }) async { final formData = { 'user_name': username, 'email': email, 'full_name': fullName, 'password': password, 'invitation_code': inviteCode, 'empty': '', 'empty2': '', }; await api.postForm('/register', formData); } Future handleTokenExpired() async { _user = null; await _clearToken(); notifyListeners(); } void logout() { handleTokenExpired(); // reuse } bool _parseIsElevated(Map json) { dynamic value = json['is_elevated'] ?? json['elevated'] ?? json['is_admin'] ?? json['admin'] ?? json['isAdmin'] ?? json['admin_user'] ?? json['role'] ?? json['roles'] ?? json['permissions'] ?? json['scopes'] ?? json['is_staff'] ?? json['staff'] ?? json['is_superuser'] ?? json['superuser'] ?? json['groups']; bool parseBoolish(dynamic v) { if (v is bool) return v; if (v is num) return v != 0; final str = v?.toString().toLowerCase().trim(); if (str == null || str.isEmpty) return false; if (['1', 'true', 'yes', 'y', 'admin', 'superuser', 'staff', 'elevated'].contains(str)) { return true; } return str.contains('admin') || str.contains('superuser') || str.contains('staff'); } if (value is List) { for (final entry in value) { if (parseBoolish(entry)) return true; final s = entry?.toString().toLowerCase(); if (s != null && (s.contains('admin') || s.contains('superuser') || s.contains('staff') || s.contains('elevated') || s == 'root')) { return true; } } return false; } return parseBoolish(value); } bool _parseIsDisabled(Map json) { dynamic value = json['disabled'] ?? json['is_disabled']; if (value is bool) return value; if (value is num) return value != 0; final str = value?.toString().toLowerCase().trim(); if (str == null || str.isEmpty) return false; return ['1', 'true', 'yes', 'y', 'disabled'].contains(str); } }