diff --git a/sigap-mobile/lib/src/cores/repositories/auth/auth_repositories.dart b/sigap-mobile/lib/src/cores/repositories/auth/auth_repositories.dart index 161c06b..830edb9 100644 --- a/sigap-mobile/lib/src/cores/repositories/auth/auth_repositories.dart +++ b/sigap-mobile/lib/src/cores/repositories/auth/auth_repositories.dart @@ -7,6 +7,7 @@ import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart'; import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart'; +import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:sigap/src/utils/exceptions/format_exceptions.dart'; import 'package:sigap/src/utils/exceptions/platform_exceptions.dart'; @@ -36,17 +37,23 @@ class AuthenticationRepository extends GetxController { if (!await _biometricService.isBiometricLoginEnabled()) { return false; } - - String? sessionString = await _biometricService.attemptBiometricLogin(); - if (sessionString == null) { + + String? refreshToken = await _biometricService.attemptBiometricLogin(); + if (refreshToken == null || refreshToken.isEmpty) { return false; } - + try { - await _supabase.auth.recoverSession(sessionString); - Get.offAllNamed('/home'); - return true; + // Use the refresh token to recover the session + final response = await _supabase.auth.refreshSession(refreshToken); + if (response.session != null) { + Get.offAllNamed(AppRoutes.explore); + return true; + } + return false; } catch (e) { + // If refresh token is invalid or expired, disable biometric login + await _biometricService.disableBiometricLogin(); return false; } } diff --git a/sigap-mobile/lib/src/cores/services/biometric_service.dart b/sigap-mobile/lib/src/cores/services/biometric_service.dart index 383b671..8bcc841 100644 --- a/sigap-mobile/lib/src/cores/services/biometric_service.dart +++ b/sigap-mobile/lib/src/cores/services/biometric_service.dart @@ -3,6 +3,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:local_auth/local_auth.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/auth/models/user_metadata_model.dart'; class BiometricService extends GetxService { static BiometricService get instance => Get.find(); @@ -80,26 +81,28 @@ class BiometricService extends GetxService { await _secureStorage.write(key: _biometricEnabledKey, value: 'true'); await _secureStorage.write(key: _userIdKey, value: user.id); - // Store the session for auto-login - final session = SupabaseService.instance.client.auth.currentSession; - if (session != null) { + // Store user email and hashed password for session recovery + final userMetadata = UserMetadataModel.fromJson(user.userMetadata); + if (userMetadata.officer?.email != null) { await _secureStorage.write( - key: _sessionKey, - value: session.toJson().toString(), + key: 'user_email', + value: userMetadata.officer?.email, ); } - // Store identifier (NIK or NRP) based on user role - final userMetadata = user.userMetadata; + // Also store the auth token, which is more secure than storing credentials + final session = SupabaseService.instance.client.auth.currentSession; + if (session != null) { + await _secureStorage.write(key: _sessionKey, value: session.accessToken); + } + + // Parse user metadata with our type-safe model String? identifier; - if (userMetadata != null) { - if (userMetadata['is_officer'] == true && - userMetadata['officer_data'] != null) { - identifier = userMetadata['officer_data']['nrp']; - } else if (userMetadata['nik'] != null) { - identifier = userMetadata['nik']; - } + if (userMetadata.isOfficer && userMetadata.officer != null) { + identifier = userMetadata.officer!.nrp; + } else if (userMetadata.nik != null) { + identifier = userMetadata.nik; } if (identifier != null) { diff --git a/sigap-mobile/lib/src/cores/services/supabase_service.dart b/sigap-mobile/lib/src/cores/services/supabase_service.dart index 964e219..105141f 100644 --- a/sigap-mobile/lib/src/cores/services/supabase_service.dart +++ b/sigap-mobile/lib/src/cores/services/supabase_service.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:sigap/src/features/auth/models/user_metadata_model.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class SupabaseService extends GetxService { @@ -15,45 +16,44 @@ class SupabaseService extends GetxService { /// Get current user ID, if authenticated String? get currentUserId => _client.auth.currentUser?.id; + /// Get type-safe user metadata + UserMetadataModel? get userMetadata { + if (currentUser == null) return null; + return UserMetadataModel.fromJson(currentUser!.userMetadata); + } + /// Check if user is authenticated bool get isAuthenticated => currentUser != null; - /// Check if session is expired - // bool get isSessionExpired => currentUser?.isExpired ?? true; - /// Check if current user is an officer based on metadata - bool get isOfficer { - final metadata = currentUser?.userMetadata; - if (metadata != null && metadata.containsKey('is_officer')) { - return metadata['is_officer'] == true; + bool get isOfficer => userMetadata?.isOfficer ?? false; + + /// Get the stored identifier (NIK or NRP) of the current user + String? get userIdentifier { + if (currentUser == null) return null; + final metadata = userMetadata; + + if (metadata?.isOfficer == true && metadata?.officer != null) { + return metadata!.officer!.nrp; + } else { + return metadata?.nik; } - return false; } /// Initialize Supabase service Future init() async { return this; } -} - -extension UserMetadataExtension on SupabaseService { - /// Update the current user's metadata - Future updateUserMetadata(Map metadata) async { - if (!isAuthenticated) { - throw Exception('User is not authenticated'); - } + /// Update user metadata with type safety + Future updateUserMetadata(UserMetadataModel metadata) async { try { - // Get existing metadata to merge with new values - final existingMetadata = currentUser?.userMetadata ?? {}; - final mergedMetadata = {...existingMetadata, ...metadata}; - - // Update user metadata - await client.auth.updateUser(UserAttributes(data: mergedMetadata)); - } on AuthException catch (e) { - throw Exception('Failed to update user metadata: ${e.message}'); + final response = await client.auth.updateUser( + UserAttributes(data: metadata.toJson()), + ); + return response.user; } catch (e) { - throw Exception('An unexpected error occurred: $e'); + throw Exception('Failed to update user metadata: $e'); } } } diff --git a/sigap-mobile/lib/src/features/auth/models/index.dart b/sigap-mobile/lib/src/features/auth/models/index.dart new file mode 100644 index 0000000..e59fea8 --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/models/index.dart @@ -0,0 +1 @@ +export 'user_metadata_model.dart'; diff --git a/sigap-mobile/lib/src/features/auth/models/user_metadata_model.dart b/sigap-mobile/lib/src/features/auth/models/user_metadata_model.dart new file mode 100644 index 0000000..bd9000e --- /dev/null +++ b/sigap-mobile/lib/src/features/auth/models/user_metadata_model.dart @@ -0,0 +1,126 @@ +import 'package:sigap/src/features/personalization/models/index.dart'; + +class UserMetadataModel { + final bool isOfficer; + final String? nik; + final OfficerModel? officer; + final Map? additionalData; + + UserMetadataModel({ + this.isOfficer = false, + this.nik, + this.officer, + this.additionalData, + }); + + /// Create a UserMetadataModel from raw Map data (from Supabase Auth) + factory UserMetadataModel.fromJson(Map? json) { + if (json == null) return UserMetadataModel(); + + return UserMetadataModel( + isOfficer: json['is_officer'] == true, + nik: json['nik'] as String?, + officer: + json['officer_data'] != null + ? OfficerModel.fromJson( + json['officer_data'] as Map, + ) + : null, + additionalData: Map.from(json)..removeWhere( + (key, _) => ['is_officer', 'nik', 'officer_data'].contains(key), + ), + ); + } + + /// Convert the model to a json Map (for Supabase Auth) + Map toJson() { + final Map data = {'is_officer': isOfficer}; + + if (nik != null) { + data['nik'] = nik; + } + + if (officer != null) { + data['officer_data'] = officer!.toJson(); + } + + if (additionalData != null) { + data.addAll(additionalData!); + } + + return data; + } + + /// Create a copy with updated fields + UserMetadataModel copyWith({ + bool? isOfficer, + String? nik, + OfficerModel? officer, + Map? additionalData, + }) { + return UserMetadataModel( + isOfficer: isOfficer ?? this.isOfficer, + nik: nik ?? this.nik, + officer: officer ?? this.officer, + additionalData: additionalData ?? this.additionalData, + ); + } +} + +// class OfficerModel { +// final String nrp; +// final String? name; +// final String? rank; +// final String? position; +// final String? phone; +// final String? unitId; + +// OfficerModel({ +// required this.nrp, +// this.name, +// this.rank, +// this.position, +// this.phone, +// this.unitId, +// }); + +// factory OfficerModel.fromJson(Map json) { +// return OfficerModel( +// nrp: json['nrp'] as String, +// name: json['name'] as String?, +// rank: json['rank'] as String?, +// position: json['position'] as String?, +// phone: json['phone'] as String?, +// unitId: json['unit_id'] as String?, +// ); +// } + +// Map toJson() { +// return { +// 'nrp': nrp, +// if (name != null) 'name': name, +// if (rank != null) 'rank': rank, +// if (position != null) 'position': position, +// if (phone != null) 'phone': phone, +// if (unitId != null) 'unit_id': unitId, +// }; +// } + +// OfficerModel copyWith({ +// String? nrp, +// String? name, +// String? rank, +// String? position, +// String? phone, +// String? unitId, +// }) { +// return OfficerModel( +// nrp: nrp ?? this.nrp, +// name: name ?? this.name, +// rank: rank ?? this.rank, +// position: position ?? this.position, +// phone: phone ?? this.phone, +// unitId: unitId ?? this.unitId, +// ); +// } +// }