import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../api/api_client.dart'; import 'permissions.dart'; import 'user_preferences.dart'; part 'auth_datasource.g.dart'; /// Response from POST /auth/sync endpoint. class AuthSyncResponse { const AuthSyncResponse({ required this.userId, required this.authentikId, required this.email, required this.name, this.avatarUrl, required this.roles, required this.preferences, required this.isNewUser, }); final String userId; final String authentikId; final String email; final String name; final String? avatarUrl; final List roles; final UserPreferences preferences; final bool isNewUser; factory AuthSyncResponse.fromJson(Map json) { final user = json['user'] as Map; final rolesJson = json['roles'] as List; final prefsJson = json['preferences'] as Map; return AuthSyncResponse( userId: user['id'] as String, authentikId: user['authentik_id'] as String, email: user['email'] as String, name: user['name'] as String, avatarUrl: user['avatar_url'] as String?, roles: rolesJson.map((r) => _parseRole(r as Map)).toList(), preferences: UserPreferences.fromJson(prefsJson), isNewUser: json['is_new_user'] as bool, ); } } /// Parse a role from API JSON. Role _parseRole(Map json) { final name = json['name'] as String; final domainStr = json['domain'] as String; final category = json['category'] as String? ?? 'general'; final actionStr = json['action'] as String; final domain = Domain.fromString(domainStr); final action = Action.fromString(actionStr); if (domain == null || action == null) { // Return a placeholder role for unknown domains/actions return Role( id: json['id'] as String, name: name, domain: Domain.admin, // Fallback category: category, action: Action.viewer, // Fallback - least privilege ); } return Role( id: json['id'] as String, name: name, domain: domain, category: category, action: action, ); } /// Datasource for auth API endpoints. class AuthDatasource { AuthDatasource(this._dio); final Dio _dio; /// Sync user with core-api after OIDC authentication. /// /// Sends the OIDC access token to core-api, which validates it with Authentik /// and returns the user profile, roles, and preferences. Future syncUser(String accessToken) async { final response = await _dio.post>( '/auth/sync', data: {'access_token': accessToken}, ); return AuthSyncResponse.fromJson(response.data!); } /// Get current user profile via NPM forward auth. /// /// This endpoint reads X-authentik-* headers set by NPM forward auth. /// Returns user profile if authenticated via the proxy. /// Throws 401 if not authenticated or accessing directly. Future getCurrentUser() async { final response = await _dio.get>('/auth/users/me'); return AuthSyncResponse.fromJson(response.data!); } /// Update user preferences. Future updatePreferences({ String? theme, String? defaultRoom, Map? preferencesJson, }) async { final data = {}; if (theme != null) data['theme'] = theme; if (defaultRoom != null) data['default_room'] = defaultRoom; if (preferencesJson != null) data['preferences_json'] = preferencesJson; final response = await _dio.patch>( '/auth/users/me/preferences', data: data, ); return UserPreferences.fromJson(response.data!); } } /// Provider for the auth datasource. @riverpod AuthDatasource authDatasource(Ref ref) { return AuthDatasource(ref.watch(coreApiClientProvider)); }