feat(auth): enhance biometric login and user metadata handling
This commit is contained in:
parent
ffed8b8ede
commit
8da86d10d2
|
@ -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';
|
||||
|
@ -37,16 +38,22 @@ class AuthenticationRepository extends GetxController {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export '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<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,
|
||||
// );
|
||||
// }
|
||||
// }
|
Loading…
Reference in New Issue