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/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/auth/screens/signin/signin_screen.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/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/exceptions.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
||||||
|
@ -37,16 +38,22 @@ class AuthenticationRepository extends GetxController {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? sessionString = await _biometricService.attemptBiometricLogin();
|
String? refreshToken = await _biometricService.attemptBiometricLogin();
|
||||||
if (sessionString == null) {
|
if (refreshToken == null || refreshToken.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _supabase.auth.recoverSession(sessionString);
|
// Use the refresh token to recover the session
|
||||||
Get.offAllNamed('/home');
|
final response = await _supabase.auth.refreshSession(refreshToken);
|
||||||
return true;
|
if (response.session != null) {
|
||||||
|
Get.offAllNamed(AppRoutes.explore);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// If refresh token is invalid or expired, disable biometric login
|
||||||
|
await _biometricService.disableBiometricLogin();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.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 {
|
class BiometricService extends GetxService {
|
||||||
static BiometricService get instance => Get.find<BiometricService>();
|
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: _biometricEnabledKey, value: 'true');
|
||||||
await _secureStorage.write(key: _userIdKey, value: user.id);
|
await _secureStorage.write(key: _userIdKey, value: user.id);
|
||||||
|
|
||||||
// Store the session for auto-login
|
// Store user email and hashed password for session recovery
|
||||||
final session = SupabaseService.instance.client.auth.currentSession;
|
final userMetadata = UserMetadataModel.fromJson(user.userMetadata);
|
||||||
if (session != null) {
|
if (userMetadata.officer?.email != null) {
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: _sessionKey,
|
key: 'user_email',
|
||||||
value: session.toJson().toString(),
|
value: userMetadata.officer?.email,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store identifier (NIK or NRP) based on user role
|
// Also store the auth token, which is more secure than storing credentials
|
||||||
final userMetadata = user.userMetadata;
|
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;
|
String? identifier;
|
||||||
|
|
||||||
if (userMetadata != null) {
|
if (userMetadata.isOfficer && userMetadata.officer != null) {
|
||||||
if (userMetadata['is_officer'] == true &&
|
identifier = userMetadata.officer!.nrp;
|
||||||
userMetadata['officer_data'] != null) {
|
} else if (userMetadata.nik != null) {
|
||||||
identifier = userMetadata['officer_data']['nrp'];
|
identifier = userMetadata.nik;
|
||||||
} else if (userMetadata['nik'] != null) {
|
|
||||||
identifier = userMetadata['nik'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identifier != null) {
|
if (identifier != null) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/models/user_metadata_model.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
class SupabaseService extends GetxService {
|
class SupabaseService extends GetxService {
|
||||||
|
@ -15,45 +16,44 @@ class SupabaseService extends GetxService {
|
||||||
/// Get current user ID, if authenticated
|
/// Get current user ID, if authenticated
|
||||||
String? get currentUserId => _client.auth.currentUser?.id;
|
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
|
/// Check if user is authenticated
|
||||||
bool get isAuthenticated => currentUser != null;
|
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
|
/// Check if current user is an officer based on metadata
|
||||||
bool get isOfficer {
|
bool get isOfficer => userMetadata?.isOfficer ?? false;
|
||||||
final metadata = currentUser?.userMetadata;
|
|
||||||
if (metadata != null && metadata.containsKey('is_officer')) {
|
/// Get the stored identifier (NIK or NRP) of the current user
|
||||||
return metadata['is_officer'] == true;
|
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
|
/// Initialize Supabase service
|
||||||
Future<SupabaseService> init() async {
|
Future<SupabaseService> init() async {
|
||||||
return this;
|
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 {
|
try {
|
||||||
// Get existing metadata to merge with new values
|
final response = await client.auth.updateUser(
|
||||||
final existingMetadata = currentUser?.userMetadata ?? {};
|
UserAttributes(data: metadata.toJson()),
|
||||||
final mergedMetadata = {...existingMetadata, ...metadata};
|
);
|
||||||
|
return response.user;
|
||||||
// Update user metadata
|
|
||||||
await client.auth.updateUser(UserAttributes(data: mergedMetadata));
|
|
||||||
} on AuthException catch (e) {
|
|
||||||
throw Exception('Failed to update user metadata: ${e.message}');
|
|
||||||
} catch (e) {
|
} 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