feat(auth): enhance biometric login and user metadata handling

This commit is contained in:
vergiLgood1 2025-05-16 22:34:23 +07:00
parent ffed8b8ede
commit 8da86d10d2
5 changed files with 183 additions and 46 deletions

View File

@ -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;
}
}

View File

@ -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<BiometricService>();
@ -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) {

View File

@ -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<SupabaseService> init() async {
return this;
}
}
extension UserMetadataExtension on SupabaseService {
/// Update the current user's metadata
Future<void> updateUserMetadata(Map<String, dynamic> metadata) async {
if (!isAuthenticated) {
throw Exception('User is not authenticated');
}
/// Update user metadata with type safety
Future<User?> 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');
}
}
}

View File

@ -0,0 +1 @@
export 'user_metadata_model.dart';

View File

@ -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<String, dynamic>? 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<String, dynamic>? 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<String, dynamic>,
)
: null,
additionalData: Map<String, dynamic>.from(json)..removeWhere(
(key, _) => ['is_officer', 'nik', 'officer_data'].contains(key),
),
);
}
/// Convert the model to a json Map (for Supabase Auth)
Map<String, dynamic> toJson() {
final Map<String, dynamic> 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<String, dynamic>? 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<String, dynamic> 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<String, dynamic> 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,
// );
// }
// }