feat: Add KTP model for Indonesian ID card representation
feat: Implement PermissionModel for role-based access control feat: Create ResourceModel to manage resources with associated permissions feat: Develop UserMetadataModel for user-related data management feat: Implement AWS Signature helper for secure API requests feat: Create detect-face function for AWS Rekognition integration feat: Implement send-email function using Resend API feat: Develop verify-face function for comparing KTP and selfie images
This commit is contained in:
parent
d9fffff68d
commit
c26d749026
|
@ -44,3 +44,8 @@ AZURE_RESOURCE_NAME="sigap"
|
||||||
AZURE_FACE_RESOURCE_NAME="verify-face"
|
AZURE_FACE_RESOURCE_NAME="verify-face"
|
||||||
AZURE_SUBSCRIPTION_KEY="ANeYAEr78MF7HzCEDg53DEHfKZJg19raPeJCubNEZP2tXGD6xREgJQQJ99BEAC3pKaRXJ3w3AAAFACOGAwA9"
|
AZURE_SUBSCRIPTION_KEY="ANeYAEr78MF7HzCEDg53DEHfKZJg19raPeJCubNEZP2tXGD6xREgJQQJ99BEAC3pKaRXJ3w3AAAFACOGAwA9"
|
||||||
AZURE_FACE_SUBSCRIPTION_KEY="6pBJKuYEFWHkrCBaZh8hErDci6ZwYnG0tEaE3VA34P8XPAYj4ZvOJQQJ99BEACqBBLyXJ3w3AAAKACOGYqeW"
|
AZURE_FACE_SUBSCRIPTION_KEY="6pBJKuYEFWHkrCBaZh8hErDci6ZwYnG0tEaE3VA34P8XPAYj4ZvOJQQJ99BEACqBBLyXJ3w3AAAKACOGYqeW"
|
||||||
|
|
||||||
|
; Aws rekognition
|
||||||
|
AWS_REGION="ap-southeast-1"
|
||||||
|
AWS_ACCESS_KEY="AKIAW3MD7UU5G2XTA44C"
|
||||||
|
AWS_SECRET_KEY="8jgxMWWmsEUd4q/++9W+R/IOQ/IxFTAKmtnaBQKe"
|
|
@ -1,10 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
|
@ -52,17 +50,8 @@ class _AnimatedSplashScreenWidgetState extends State<AnimatedSplashScreenWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleNavigation() async {
|
Future<void> _handleNavigation() async {
|
||||||
// Check if onboarding is completed
|
|
||||||
final isFirstTime = storage.read('isFirstTime') ?? false;
|
|
||||||
|
|
||||||
if (isFirstTime) {
|
|
||||||
// Navigate to onboarding if it's the first time
|
|
||||||
Get.offAll(() => const OnboardingScreen());
|
|
||||||
} else {
|
|
||||||
// Use the authentication repository to determine where to navigate
|
|
||||||
AuthenticationRepository.instance.screenRedirect();
|
AuthenticationRepository.instance.screenRedirect();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/api_urls.dart';
|
||||||
|
import 'package:sigap/src/utils/dio.client/dio_client.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/aws_signature.dart';
|
||||||
|
|
||||||
|
class AwsRecognitionService {
|
||||||
|
// Singleton instance
|
||||||
|
static final AwsRecognitionService instance = AwsRecognitionService._();
|
||||||
|
AwsRecognitionService._();
|
||||||
|
|
||||||
|
// AWS Recognition API configuration
|
||||||
|
final String region = Endpoints.awsRegion;
|
||||||
|
final String accessKey = Endpoints.awsAccessKey;
|
||||||
|
final String secretKey = Endpoints.awsSecretKey;
|
||||||
|
final String serviceEndpoint = Endpoints.awsRekognitionEndpoint;
|
||||||
|
final String serviceName = 'rekognition';
|
||||||
|
|
||||||
|
// Face detection threshold values
|
||||||
|
final double faceMatchThreshold =
|
||||||
|
80.0; // Minimum confidence for face match (0-100)
|
||||||
|
|
||||||
|
// Detect faces in an image and return face details
|
||||||
|
Future<List<FaceModel>> detectFaces(XFile imageFile) async {
|
||||||
|
try {
|
||||||
|
final bytes = await File(imageFile.path).readAsBytes();
|
||||||
|
final base64Image = base64Encode(bytes);
|
||||||
|
|
||||||
|
// Create AWS Signature
|
||||||
|
final awsSignature = AwsSignature(
|
||||||
|
accessKey: accessKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
region: region,
|
||||||
|
serviceName: serviceName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prepare request payload
|
||||||
|
final payload = {
|
||||||
|
'Image': {'Bytes': base64Image},
|
||||||
|
'Attributes': ['DEFAULT'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get signed headers and URL
|
||||||
|
final dateTime = DateTime.now().toUtc();
|
||||||
|
final uri = Uri.parse('$serviceEndpoint/DetectFaces');
|
||||||
|
final headers = awsSignature.buildRequestHeaders(
|
||||||
|
method: 'POST',
|
||||||
|
uri: uri,
|
||||||
|
payload: payload,
|
||||||
|
dateTime: dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make API request
|
||||||
|
final response = await DioClient().post(
|
||||||
|
uri.toString(),
|
||||||
|
data: payload,
|
||||||
|
options: Options(headers: headers, responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final faceDetails = response.data['FaceDetails'];
|
||||||
|
// Convert AWS response to FaceModel objects
|
||||||
|
List<FaceModel> faces = [];
|
||||||
|
for (var i = 0; i < faceDetails.length; i++) {
|
||||||
|
String faceId = 'face_${dateTime.millisecondsSinceEpoch}_$i';
|
||||||
|
faces.add(FaceModel.fromDetection(faceId, imageFile, faceDetails[i]));
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to detect faces: ${response.statusCode} - ${response.data}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Face detection error: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two face images and return comparison result
|
||||||
|
Future<FaceComparisonResult> compareFaces(
|
||||||
|
XFile sourceImage,
|
||||||
|
XFile targetImage,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// First detect faces in both images
|
||||||
|
List<FaceModel> sourceFaces = await detectFaces(sourceImage);
|
||||||
|
List<FaceModel> targetFaces = await detectFaces(targetImage);
|
||||||
|
|
||||||
|
if (sourceFaces.isEmpty || targetFaces.isEmpty) {
|
||||||
|
return FaceComparisonResult.noMatch(
|
||||||
|
sourceFaces.isEmpty ? FaceModel.empty() : sourceFaces.first,
|
||||||
|
targetFaces.isEmpty ? FaceModel.empty() : targetFaces.first,
|
||||||
|
message:
|
||||||
|
sourceFaces.isEmpty && targetFaces.isEmpty
|
||||||
|
? 'No faces detected in either image'
|
||||||
|
: sourceFaces.isEmpty
|
||||||
|
? 'No face detected in ID card image'
|
||||||
|
: 'No face detected in selfie image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the primary faces from each image
|
||||||
|
FaceModel sourceFace = sourceFaces.first;
|
||||||
|
FaceModel targetFace = targetFaces.first;
|
||||||
|
|
||||||
|
final sourceBytes = await File(sourceImage.path).readAsBytes();
|
||||||
|
final targetBytes = await File(targetImage.path).readAsBytes();
|
||||||
|
|
||||||
|
// Create AWS Signature
|
||||||
|
final awsSignature = AwsSignature(
|
||||||
|
accessKey: accessKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
region: region,
|
||||||
|
serviceName: serviceName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prepare request payload
|
||||||
|
final payload = {
|
||||||
|
'SourceImage': {'Bytes': base64Encode(sourceBytes)},
|
||||||
|
'TargetImage': {'Bytes': base64Encode(targetBytes)},
|
||||||
|
'SimilarityThreshold': faceMatchThreshold,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get signed headers and URL
|
||||||
|
final dateTime = DateTime.now().toUtc();
|
||||||
|
final uri = Uri.parse('$serviceEndpoint/CompareFaces');
|
||||||
|
final headers = awsSignature.buildRequestHeaders(
|
||||||
|
method: 'POST',
|
||||||
|
uri: uri,
|
||||||
|
payload: payload,
|
||||||
|
dateTime: dateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make API request
|
||||||
|
final response = await DioClient().post(
|
||||||
|
uri.toString(),
|
||||||
|
data: payload,
|
||||||
|
options: Options(headers: headers, responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return FaceComparisonResult.fromAwsResponse(
|
||||||
|
sourceFace,
|
||||||
|
targetFace,
|
||||||
|
response.data,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to compare faces: ${response.statusCode} - ${response.data}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Face comparison error: $e');
|
||||||
|
return FaceComparisonResult.error(
|
||||||
|
FaceModel.empty().withMessage('Source face processing error'),
|
||||||
|
FaceModel.empty().withMessage('Target face processing error'),
|
||||||
|
e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform liveness detection (anti-spoofing check)
|
||||||
|
Future<FaceModel> performLivenessCheck(XFile selfieImage) async {
|
||||||
|
try {
|
||||||
|
// In a real implementation, AWS Recognition doesn't directly offer liveness detection
|
||||||
|
// You might need to use a combination of services or a third-party solution
|
||||||
|
// For now, we'll simulate a successful check by detecting a face
|
||||||
|
final faces = await detectFaces(selfieImage);
|
||||||
|
|
||||||
|
if (faces.isEmpty) {
|
||||||
|
return FaceModel.empty().withLiveness(
|
||||||
|
isLive: false,
|
||||||
|
confidence: 0.0,
|
||||||
|
message: 'No face detected in the selfie.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the primary face
|
||||||
|
FaceModel face = faces.first;
|
||||||
|
|
||||||
|
// Check confidence of face detection as a basic indicator
|
||||||
|
if (face.detectionConfidence < 0.7) {
|
||||||
|
return face.withLiveness(
|
||||||
|
isLive: false,
|
||||||
|
confidence: face.detectionConfidence,
|
||||||
|
message:
|
||||||
|
'Low confidence face detection. Please take a clearer selfie.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a full implementation, you might want to:
|
||||||
|
// 1. Check eye blink detection
|
||||||
|
// 2. Analyze multiple facial movements
|
||||||
|
// 3. Use depth information if available
|
||||||
|
|
||||||
|
return face.withLiveness(
|
||||||
|
isLive: true,
|
||||||
|
confidence: face.detectionConfidence,
|
||||||
|
message: 'Liveness check passed successfully.',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return FaceModel.empty().withLiveness(
|
||||||
|
isLive: false,
|
||||||
|
confidence: 0.0,
|
||||||
|
message: 'Liveness check error: ${e.toString()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
import 'package:sigap/src/utils/constants/api_urls.dart';
|
import 'package:sigap/src/utils/constants/api_urls.dart';
|
||||||
import 'package:sigap/src/utils/dio.client/dio_client.dart';
|
import 'package:sigap/src/utils/dio.client/dio_client.dart';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +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/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/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>();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/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 {
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
/// A model class that represents facial recognition data and metrics
|
||||||
|
class FaceModel {
|
||||||
|
/// Unique identifier for the face
|
||||||
|
final String faceId;
|
||||||
|
|
||||||
|
/// Source image that contains the face
|
||||||
|
final XFile? sourceImage;
|
||||||
|
|
||||||
|
/// Raw face details as returned from AWS Rekognition
|
||||||
|
final Map<String, dynamic> faceDetails;
|
||||||
|
|
||||||
|
/// Confidence score for face detection (0.0-1.0)
|
||||||
|
final double detectionConfidence;
|
||||||
|
|
||||||
|
/// Whether the face passed liveness detection
|
||||||
|
final bool isLive;
|
||||||
|
|
||||||
|
/// Liveness detection confidence (0.0-1.0)
|
||||||
|
final double livenessConfidence;
|
||||||
|
|
||||||
|
/// Whether this face matches another face (after comparison)
|
||||||
|
final bool isMatch;
|
||||||
|
|
||||||
|
/// Match confidence with comparison face (0.0-1.0)
|
||||||
|
final double matchConfidence;
|
||||||
|
|
||||||
|
/// Message providing details about the face status
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Creates a FaceModel with the given parameters
|
||||||
|
const FaceModel({
|
||||||
|
required this.faceId,
|
||||||
|
this.sourceImage,
|
||||||
|
this.faceDetails = const {},
|
||||||
|
this.detectionConfidence = 0.0,
|
||||||
|
this.isLive = false,
|
||||||
|
this.livenessConfidence = 0.0,
|
||||||
|
this.isMatch = false,
|
||||||
|
this.matchConfidence = 0.0,
|
||||||
|
this.message = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a FaceModel from AWS Rekognition detection response
|
||||||
|
factory FaceModel.fromDetection(
|
||||||
|
String faceId,
|
||||||
|
XFile sourceImage,
|
||||||
|
Map<String, dynamic> detectionData,
|
||||||
|
) {
|
||||||
|
final double confidence = (detectionData['Confidence'] ?? 0.0) / 100.0;
|
||||||
|
|
||||||
|
return FaceModel(
|
||||||
|
faceId: faceId,
|
||||||
|
sourceImage: sourceImage,
|
||||||
|
faceDetails: detectionData,
|
||||||
|
detectionConfidence: confidence,
|
||||||
|
message:
|
||||||
|
'Face detected with ${(confidence * 100).toStringAsFixed(1)}% confidence',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a FaceModel with liveness details
|
||||||
|
FaceModel withLiveness({
|
||||||
|
required bool isLive,
|
||||||
|
required double confidence,
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return FaceModel(
|
||||||
|
faceId: faceId,
|
||||||
|
sourceImage: sourceImage,
|
||||||
|
faceDetails: faceDetails,
|
||||||
|
detectionConfidence: detectionConfidence,
|
||||||
|
isLive: isLive,
|
||||||
|
livenessConfidence: confidence,
|
||||||
|
isMatch: isMatch,
|
||||||
|
matchConfidence: matchConfidence,
|
||||||
|
message: message ?? this.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a FaceModel with match details
|
||||||
|
FaceModel withMatch({
|
||||||
|
required bool isMatch,
|
||||||
|
required double confidence,
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return FaceModel(
|
||||||
|
faceId: faceId,
|
||||||
|
sourceImage: sourceImage,
|
||||||
|
faceDetails: faceDetails,
|
||||||
|
detectionConfidence: detectionConfidence,
|
||||||
|
isLive: isLive,
|
||||||
|
livenessConfidence: livenessConfidence,
|
||||||
|
isMatch: isMatch,
|
||||||
|
matchConfidence: confidence,
|
||||||
|
message: message ?? this.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the message for this FaceModel
|
||||||
|
FaceModel withMessage(String newMessage) {
|
||||||
|
return FaceModel(
|
||||||
|
faceId: faceId,
|
||||||
|
sourceImage: sourceImage,
|
||||||
|
faceDetails: faceDetails,
|
||||||
|
detectionConfidence: detectionConfidence,
|
||||||
|
isLive: isLive,
|
||||||
|
livenessConfidence: livenessConfidence,
|
||||||
|
isMatch: isMatch,
|
||||||
|
matchConfidence: matchConfidence,
|
||||||
|
message: newMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an empty FaceModel with no data
|
||||||
|
factory FaceModel.empty() {
|
||||||
|
return const FaceModel(faceId: '', message: 'No face data available');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this FaceModel instance has valid face data
|
||||||
|
bool get hasValidFace => faceId.isNotEmpty && detectionConfidence > 0.5;
|
||||||
|
|
||||||
|
/// Returns age range if available in faceDetails
|
||||||
|
Map<String, int>? get ageRange {
|
||||||
|
if (faceDetails.containsKey('AgeRange')) {
|
||||||
|
return {
|
||||||
|
'low': faceDetails['AgeRange']['Low'] ?? 0,
|
||||||
|
'high': faceDetails['AgeRange']['High'] ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns gender information if available
|
||||||
|
String? get gender => faceDetails['Gender']?['Value'];
|
||||||
|
|
||||||
|
/// Returns whether the person is smiling if available
|
||||||
|
bool? get isSmiling => faceDetails['Smile']?['Value'];
|
||||||
|
|
||||||
|
/// Returns a map representation of this model
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'faceId': faceId,
|
||||||
|
'detectionConfidence': detectionConfidence,
|
||||||
|
'isLive': isLive,
|
||||||
|
'livenessConfidence': livenessConfidence,
|
||||||
|
'isMatch': isMatch,
|
||||||
|
'matchConfidence': matchConfidence,
|
||||||
|
'message': message,
|
||||||
|
'hasValidFace': hasValidFace,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a face comparison between two faces
|
||||||
|
class FaceComparisonResult {
|
||||||
|
/// The source face in the comparison
|
||||||
|
final FaceModel sourceFace;
|
||||||
|
|
||||||
|
/// The target face in the comparison
|
||||||
|
final FaceModel targetFace;
|
||||||
|
|
||||||
|
/// Whether the faces match
|
||||||
|
final bool isMatch;
|
||||||
|
|
||||||
|
/// Confidence level of the match (0.0-1.0)
|
||||||
|
final double confidence;
|
||||||
|
|
||||||
|
/// Message describing the comparison result
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Creates a FaceComparisonResult with the given parameters
|
||||||
|
const FaceComparisonResult({
|
||||||
|
required this.sourceFace,
|
||||||
|
required this.targetFace,
|
||||||
|
required this.isMatch,
|
||||||
|
required this.confidence,
|
||||||
|
required this.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a FaceComparisonResult from AWS comparison response
|
||||||
|
factory FaceComparisonResult.fromAwsResponse(
|
||||||
|
FaceModel sourceFace,
|
||||||
|
FaceModel targetFace,
|
||||||
|
Map<String, dynamic> response,
|
||||||
|
) {
|
||||||
|
bool isMatch = false;
|
||||||
|
double confidence = 0.0;
|
||||||
|
String message = 'Face comparison failed';
|
||||||
|
|
||||||
|
if (response['FaceMatches'] != null && response['FaceMatches'].isNotEmpty) {
|
||||||
|
final match = response['FaceMatches'][0];
|
||||||
|
confidence = (match['Similarity'] ?? 0.0) / 100.0;
|
||||||
|
isMatch = confidence >= 0.8; // 80% threshold
|
||||||
|
message =
|
||||||
|
isMatch
|
||||||
|
? 'Face verification successful! Confidence: ${(confidence * 100).toStringAsFixed(1)}%'
|
||||||
|
: 'Face similarity too low: ${(confidence * 100).toStringAsFixed(1)}%';
|
||||||
|
} else {
|
||||||
|
message = 'No matching faces found';
|
||||||
|
}
|
||||||
|
|
||||||
|
return FaceComparisonResult(
|
||||||
|
sourceFace: sourceFace.withMatch(
|
||||||
|
isMatch: isMatch,
|
||||||
|
confidence: confidence,
|
||||||
|
),
|
||||||
|
targetFace: targetFace.withMatch(
|
||||||
|
isMatch: isMatch,
|
||||||
|
confidence: confidence,
|
||||||
|
),
|
||||||
|
isMatch: isMatch,
|
||||||
|
confidence: confidence,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an empty result with no match
|
||||||
|
factory FaceComparisonResult.noMatch(
|
||||||
|
FaceModel sourceFace,
|
||||||
|
FaceModel targetFace, {
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return FaceComparisonResult(
|
||||||
|
sourceFace: sourceFace,
|
||||||
|
targetFace: targetFace,
|
||||||
|
isMatch: false,
|
||||||
|
confidence: 0.0,
|
||||||
|
message: message ?? 'No matching faces',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an error result
|
||||||
|
factory FaceComparisonResult.error(
|
||||||
|
FaceModel sourceFace,
|
||||||
|
FaceModel targetFace,
|
||||||
|
String errorMessage,
|
||||||
|
) {
|
||||||
|
return FaceComparisonResult(
|
||||||
|
sourceFace: sourceFace,
|
||||||
|
targetFace: targetFace,
|
||||||
|
isMatch: false,
|
||||||
|
confidence: 0.0,
|
||||||
|
message: 'Error: $errorMessage',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
export 'user_metadata_model.dart';
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/resources_model.dart';
|
import 'package:sigap/src/features/auth/data/models/resources_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
||||||
|
|
||||||
class PermissionModel {
|
class PermissionModel {
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/permissions_model.dart';
|
import 'package:sigap/src/features/auth/data/models/permissions_model.dart';
|
||||||
|
|
||||||
class ResourceModel {
|
class ResourceModel {
|
||||||
final String id;
|
final String id;
|
|
@ -5,7 +5,7 @@ import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/cores/services/biometric_service.dart';
|
import 'package:sigap/src/cores/services/biometric_service.dart';
|
||||||
import 'package:sigap/src/cores/services/location_service.dart';
|
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/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.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';
|
||||||
|
@ -65,16 +65,30 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _isRedirecting = false;
|
||||||
|
|
||||||
/// Updated screenRedirect method to handle onboarding preferences
|
/// Updated screenRedirect method to handle onboarding preferences
|
||||||
void screenRedirect({UserMetadataModel? arguments}) async {
|
void screenRedirect({UserMetadataModel? arguments}) async {
|
||||||
|
// Prevent recursive calls with a static guard
|
||||||
|
if (_isRedirecting) {
|
||||||
|
Logger().w('Screen redirect already in progress, ignoring this call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_isRedirecting = true;
|
||||||
|
Logger().d('Starting screen redirect');
|
||||||
|
|
||||||
final session = _supabase.auth.currentSession;
|
final session = _supabase.auth.currentSession;
|
||||||
final bool isFirstTime = storage.read('isFirstTime') ?? false;
|
final bool isFirstTime = storage.read('isFirstTime') ?? true;
|
||||||
final isEmailVerified = session?.user.emailConfirmedAt != null;
|
final isEmailVerified = session?.user.emailConfirmedAt != null;
|
||||||
final isProfileComplete =
|
final isProfileComplete =
|
||||||
session?.user.userMetadata?['profile_status'] == 'complete';
|
session?.user.userMetadata?['profile_status'] == 'complete';
|
||||||
|
|
||||||
// Logger().i('isFirstTime screen redirect: $isFirstTime');
|
// Log the current state for debugging
|
||||||
|
Logger().d(
|
||||||
|
'Screen redirect state - Session: ${session != null}, Email verified: $isEmailVerified, Profile complete: $isProfileComplete, First time: $isFirstTime, Current route: ${Get.currentRoute}',
|
||||||
|
);
|
||||||
|
|
||||||
// Cek lokasi terlebih dahulu
|
// Cek lokasi terlebih dahulu
|
||||||
if (await _locationService.isLocationValidForFeature() == false) {
|
if (await _locationService.isLocationValidForFeature() == false) {
|
||||||
|
@ -86,6 +100,11 @@ class AuthenticationRepository extends GetxController {
|
||||||
if (!isEmailVerified) {
|
if (!isEmailVerified) {
|
||||||
_navigateToRoute(AppRoutes.emailVerification);
|
_navigateToRoute(AppRoutes.emailVerification);
|
||||||
} else if (!isProfileComplete && isEmailVerified) {
|
} else if (!isProfileComplete && isEmailVerified) {
|
||||||
|
// If already on registration form, don't redirect again
|
||||||
|
if (Get.currentRoute == AppRoutes.registrationForm) {
|
||||||
|
Logger().d('Already on registration form, not redirecting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
_navigateToRoute(AppRoutes.registrationForm);
|
_navigateToRoute(AppRoutes.registrationForm);
|
||||||
} else {
|
} else {
|
||||||
_navigateToRoute(AppRoutes.panicButton);
|
_navigateToRoute(AppRoutes.panicButton);
|
||||||
|
@ -96,13 +115,20 @@ class AuthenticationRepository extends GetxController {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error in screenRedirect: $e');
|
Logger().e('Error in screenRedirect: $e');
|
||||||
_navigateToRoute(AppRoutes.signIn);
|
_navigateToRoute(AppRoutes.signIn);
|
||||||
|
} finally {
|
||||||
|
_isRedirecting = false;
|
||||||
|
Logger().d('Screen redirect completed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToRoute(String routeName) {
|
void _navigateToRoute(String routeName) {
|
||||||
if (Get.currentRoute != routeName) {
|
// Check if already on this route to avoid navigation loops
|
||||||
Get.offAllNamed(routeName);
|
if (Get.currentRoute == routeName) {
|
||||||
|
Logger().d('Already on $routeName, skipping navigation');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
Logger().d('Navigating to: $routeName');
|
||||||
|
Get.offAllNamed(routeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pisahkan logic untuk user yang belum login
|
// Pisahkan logic untuk user yang belum login
|
||||||
|
@ -118,7 +144,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// EMAIL & PASSWORD AUTHENTICATION
|
// EMAIL & PASSWORD AUTHENTICATION
|
||||||
|
@ -582,6 +608,11 @@ class AuthenticationRepository extends GetxController {
|
||||||
|
|
||||||
/// Updates user profile after registration form completion
|
/// Updates user profile after registration form completion
|
||||||
Future<void> completeUserProfile(UserMetadataModel completeData) async {
|
Future<void> completeUserProfile(UserMetadataModel completeData) async {
|
||||||
|
if (_isRedirecting) {
|
||||||
|
// Don't attempt profile completion while already redirecting
|
||||||
|
throw 'Cannot complete profile during redirection. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert to UserModel
|
// Convert to UserModel
|
||||||
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
||||||
|
@ -604,6 +635,9 @@ class AuthenticationRepository extends GetxController {
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.insert(completeData.viewerData!.toJson());
|
.insert(completeData.viewerData!.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set redirection flag to ensure we don't navigate before setup is complete
|
||||||
|
_isRedirecting = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw 'Failed to update profile: ${e.toString()}';
|
throw 'Failed to update profile: ${e.toString()}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/identity_verification_controller.dart';
|
||||||
|
@ -12,6 +12,8 @@ import 'package:sigap/src/features/auth/presentasion/controllers/steps/selfie_ve
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/unit_info_controller.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/constants/num_int.dart';
|
import 'package:sigap/src/utils/constants/num_int.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
@ -50,7 +52,6 @@ class FormRegistrationController extends GetxController {
|
||||||
// Officer data
|
// Officer data
|
||||||
final Rx<OfficerModel?> officerModel = Rx<OfficerModel?>(null);
|
final Rx<OfficerModel?> officerModel = Rx<OfficerModel?>(null);
|
||||||
|
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
|
@ -60,8 +61,127 @@ class FormRegistrationController extends GetxController {
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// Initialize user data directly from current session
|
// Initialize user data directly from current session without triggering redirects
|
||||||
_initializeFromCurrentUser();
|
_initializeSafely();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize safely without triggering redirects
|
||||||
|
void _initializeSafely() {
|
||||||
|
// First initialize form controllers to prevent null errors
|
||||||
|
_initializeControllers();
|
||||||
|
|
||||||
|
// Then fetch user data in the background
|
||||||
|
Future.microtask(() => _fetchUserDataOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch user data without triggering screen redirects
|
||||||
|
Future<void> _fetchUserDataOnly() async {
|
||||||
|
try {
|
||||||
|
Logger().d('Fetching user data safely without redirects');
|
||||||
|
|
||||||
|
// Get user session directly without going through AuthRepository methods that might trigger redirects
|
||||||
|
final session = SupabaseService.instance.client.auth.currentSession;
|
||||||
|
|
||||||
|
if (session?.user != null) {
|
||||||
|
final user = session!.user;
|
||||||
|
Logger().d('Found user session: ${user.id} - ${user.email}');
|
||||||
|
|
||||||
|
// Extract basic metadata
|
||||||
|
UserMetadataModel metadata = UserMetadataModel(
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roleId: user.userMetadata?['role_id'] as String?,
|
||||||
|
isOfficer: user.userMetadata?['is_officer'] as bool? ?? false,
|
||||||
|
profileStatus:
|
||||||
|
user.userMetadata?['profile_status'] as String? ?? 'incomplete',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to parse complete metadata if available
|
||||||
|
if (user.userMetadata != null) {
|
||||||
|
try {
|
||||||
|
final fullMetadata = UserMetadataModel.fromJson(user.userMetadata);
|
||||||
|
metadata = fullMetadata;
|
||||||
|
Logger().d('Successfully parsed complete user metadata');
|
||||||
|
} catch (e) {
|
||||||
|
Logger().w('Could not parse full metadata: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the metadata value
|
||||||
|
userMetadata.value = metadata;
|
||||||
|
Logger().d('User metadata set: ${userMetadata.value.toString()}');
|
||||||
|
|
||||||
|
// Complete initialization without triggering redirects
|
||||||
|
await _completeInitialization();
|
||||||
|
} else {
|
||||||
|
// Check arguments and temp storage if no active session
|
||||||
|
_handleNoActiveSession();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error fetching user data: $e');
|
||||||
|
// Set default values
|
||||||
|
userMetadata.value = const UserMetadataModel(
|
||||||
|
profileStatus: 'incomplete',
|
||||||
|
isOfficer: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle case when there's no active session
|
||||||
|
void _handleNoActiveSession() {
|
||||||
|
try {
|
||||||
|
final arguments = Get.arguments;
|
||||||
|
|
||||||
|
if (arguments is Map<String, dynamic> &&
|
||||||
|
arguments.containsKey('userId')) {
|
||||||
|
userMetadata.value = UserMetadataModel(
|
||||||
|
userId: arguments['userId'] as String?,
|
||||||
|
email: arguments['email'] as String?,
|
||||||
|
roleId: arguments['roleId'] as String?,
|
||||||
|
isOfficer: arguments['isOfficer'] as bool? ?? false,
|
||||||
|
profileStatus: 'incomplete',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Check temporary storage
|
||||||
|
final tempUserId = storage.read('TEMP_USER_ID') as String?;
|
||||||
|
final tempEmail = storage.read('CURRENT_USER_EMAIL') as String?;
|
||||||
|
|
||||||
|
if (tempUserId != null || tempEmail != null) {
|
||||||
|
userMetadata.value = UserMetadataModel(
|
||||||
|
userId: tempUserId,
|
||||||
|
email: tempEmail,
|
||||||
|
roleId: storage.read('TEMP_ROLE_ID') as String?,
|
||||||
|
isOfficer: storage.read('IS_OFFICER') as bool? ?? false,
|
||||||
|
profileStatus: 'incomplete',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete initialization without triggering redirects
|
||||||
|
_completeInitialization();
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error handling no active session: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete initialization without triggering redirects
|
||||||
|
Future<void> _completeInitialization() async {
|
||||||
|
try {
|
||||||
|
// Set role if available
|
||||||
|
if (userMetadata.value.roleId?.isNotEmpty == true) {
|
||||||
|
await _setRoleFromMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch units if user is an officer
|
||||||
|
if (userMetadata.value.isOfficer ||
|
||||||
|
(selectedRole.value?.isOfficer == true)) {
|
||||||
|
await _fetchAvailableUnits();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger().d('Initialization completed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error completing initialization: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the controller directly from current user session
|
/// Initialize the controller directly from current user session
|
||||||
|
@ -105,53 +225,19 @@ class FormRegistrationController extends GetxController {
|
||||||
// Continue with the basic metadata already created
|
// Continue with the basic metadata already created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// No active session, check if any arguments were passed
|
|
||||||
final arguments = Get.arguments;
|
|
||||||
|
|
||||||
// If arguments contain a user ID, use it as fallback
|
|
||||||
if (arguments is Map<String, dynamic> &&
|
|
||||||
arguments.containsKey('userId')) {
|
|
||||||
metadata = metadata.copyWith(
|
|
||||||
userId: arguments['userId'] as String?,
|
|
||||||
email: arguments['email'] as String?,
|
|
||||||
roleId: arguments['roleId'] as String?,
|
|
||||||
isOfficer: arguments['isOfficer'] as bool? ?? false,
|
|
||||||
);
|
|
||||||
Logger().d('Using arguments as fallback: ${metadata.userId}');
|
|
||||||
} else {
|
|
||||||
// No user data available, check temporary storage
|
|
||||||
final tempUserId = storage.read('TEMP_USER_ID') as String?;
|
|
||||||
final tempEmail = storage.read('CURRENT_USER_EMAIL') as String?;
|
|
||||||
|
|
||||||
if (tempUserId != null || tempEmail != null) {
|
|
||||||
metadata = metadata.copyWith(
|
|
||||||
userId: tempUserId,
|
|
||||||
email: tempEmail,
|
|
||||||
roleId: storage.read('TEMP_ROLE_ID') as String?,
|
|
||||||
isOfficer: storage.read('TEMP_IS_OFFICER') as bool? ?? false,
|
|
||||||
);
|
|
||||||
Logger().d(
|
|
||||||
'Using temporary storage as fallback: ${metadata.userId}',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Logger().w('No user data available, using default empty metadata');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the user metadata
|
// Set the user metadata
|
||||||
userMetadata.value = metadata;
|
userMetadata.value = metadata;
|
||||||
Logger().d('Final user metadata: ${userMetadata.value.toString()}');
|
Logger().d('Final user metadata: ${userMetadata.value.toString()}');
|
||||||
|
|
||||||
// Complete initialization
|
|
||||||
await _finalizeInitialization();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error initializing from current user: $e');
|
Logger().e('Error initializing from current user: $e');
|
||||||
userMetadata.value = const UserMetadataModel(
|
userMetadata.value = const UserMetadataModel(
|
||||||
profileStatus: 'incomplete',
|
profileStatus: 'incomplete',
|
||||||
isOfficer: false,
|
isOfficer: false,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
// Complete initialization
|
||||||
await _finalizeInitialization();
|
await _finalizeInitialization();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,10 +249,18 @@ class FormRegistrationController extends GetxController {
|
||||||
_initializeControllers();
|
_initializeControllers();
|
||||||
|
|
||||||
// Set role information if available
|
// Set role information if available
|
||||||
if (userMetadata.value.roleId?.isNotEmpty == true) {
|
if (userMetadata.value.roleId == null ||
|
||||||
await _setRoleFromMetadata();
|
userMetadata.value.roleId!.isEmpty) {
|
||||||
|
// If no role ID is found, show an error message
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Role ID not found. Please contact support.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set role based on metadata
|
||||||
|
await _setRoleFromMetadata();
|
||||||
|
|
||||||
// Fetch units if user is an officer
|
// Fetch units if user is an officer
|
||||||
if (userMetadata.value.isOfficer ||
|
if (userMetadata.value.isOfficer ||
|
||||||
(selectedRole.value?.isOfficer == true)) {
|
(selectedRole.value?.isOfficer == true)) {
|
||||||
|
@ -183,34 +277,49 @@ class FormRegistrationController extends GetxController {
|
||||||
Future<void> _setRoleFromMetadata() async {
|
Future<void> _setRoleFromMetadata() async {
|
||||||
try {
|
try {
|
||||||
final roleId = userMetadata.value.roleId;
|
final roleId = userMetadata.value.roleId;
|
||||||
if (roleId?.isNotEmpty == true) {
|
if (roleId == null) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Role ID not found. Please contact support.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to find the role in available roles
|
// Try to find the role in available roles
|
||||||
final role = await _findRoleById(roleId!);
|
final role = await RolesRepository.instance.getRoleById(roleId);
|
||||||
if (role != null) {
|
|
||||||
selectedRole.value = role;
|
selectedRole.value = role;
|
||||||
Logger().d('Role set from metadata: ${role.name}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error setting role from metadata: $e');
|
Logger().e('Error setting role from metadata: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find role by ID (implement based on your role management system)
|
|
||||||
Future<RoleModel?> _findRoleById(String roleId) async {
|
|
||||||
try {
|
|
||||||
// Implement based on your role fetching logic
|
|
||||||
// This is a placeholder - replace with your actual implementation
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
Logger().e('Error finding role by ID: $e');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
final isOfficer = userMetadata.value.isOfficer;
|
final isOfficer = userMetadata.value.isOfficer;
|
||||||
|
|
||||||
|
// Clear existing controllers first to prevent duplicates
|
||||||
|
if (Get.isRegistered<PersonalInfoController>()) {
|
||||||
|
Get.delete<PersonalInfoController>(force: true);
|
||||||
|
}
|
||||||
|
if (Get.isRegistered<IdCardVerificationController>()) {
|
||||||
|
Get.delete<IdCardVerificationController>(force: true);
|
||||||
|
}
|
||||||
|
if (Get.isRegistered<SelfieVerificationController>()) {
|
||||||
|
Get.delete<SelfieVerificationController>(force: true);
|
||||||
|
}
|
||||||
|
if (Get.isRegistered<IdentityVerificationController>()) {
|
||||||
|
Get.delete<IdentityVerificationController>(force: true);
|
||||||
|
}
|
||||||
|
if (Get.isRegistered<OfficerInfoController>()) {
|
||||||
|
Get.delete<OfficerInfoController>(force: true);
|
||||||
|
}
|
||||||
|
if (Get.isRegistered<UnitInfoController>()) {
|
||||||
|
Get.delete<UnitInfoController>(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize form key if not already initialized
|
||||||
|
formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// Initialize controllers with built-in static form keys
|
// Initialize controllers with built-in static form keys
|
||||||
Get.put<PersonalInfoController>(PersonalInfoController(), permanent: false);
|
Get.put<PersonalInfoController>(PersonalInfoController(), permanent: false);
|
||||||
|
|
||||||
|
@ -232,7 +341,6 @@ class FormRegistrationController extends GetxController {
|
||||||
// Initialize officer-specific controllers only if user is an officer
|
// Initialize officer-specific controllers only if user is an officer
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
||||||
|
|
||||||
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
||||||
|
|
||||||
totalSteps =
|
totalSteps =
|
||||||
|
@ -254,7 +362,7 @@ class FormRegistrationController extends GetxController {
|
||||||
selfieVerificationController = Get.find<SelfieVerificationController>();
|
selfieVerificationController = Get.find<SelfieVerificationController>();
|
||||||
identityController = Get.find<IdentityVerificationController>();
|
identityController = Get.find<IdentityVerificationController>();
|
||||||
|
|
||||||
// Initialize selectedRole based on isOfficer
|
// Initialize selectedRole based on isOfficer if not already set
|
||||||
if (selectedRole.value == null &&
|
if (selectedRole.value == null &&
|
||||||
userMetadata.value.additionalData != null) {
|
userMetadata.value.additionalData != null) {
|
||||||
final roleData = userMetadata.value.additionalData?['role'];
|
final roleData = userMetadata.value.additionalData?['role'];
|
||||||
|
@ -395,6 +503,7 @@ class FormRegistrationController extends GetxController {
|
||||||
submitForm();
|
submitForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearPreviousStepErrors() {
|
void clearPreviousStepErrors() {
|
||||||
switch (currentStep.value) {
|
switch (currentStep.value) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||||
|
@ -243,7 +243,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
|
|
||||||
// Navigate with arguments
|
// Navigate with arguments
|
||||||
Logger().i('Navigating to registration form');
|
Logger().i('Navigating to registration form');
|
||||||
// AuthenticationRepository.instance.screenRedirect();
|
AuthenticationRepository.instance.screenRedirect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error during signup: $e');
|
Logger().e('Error during signup: $e');
|
||||||
String errorMessage = _getReadableErrorMessage(e.toString());
|
String errorMessage = _getReadableErrorMessage(e.toString());
|
||||||
|
|
|
@ -2,17 +2,21 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:sigap/src/cores/services/aws_rekognition_service.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
|
|
||||||
class IdCardVerificationController extends GetxController {
|
class IdCardVerificationController extends GetxController {
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
static IdCardVerificationController get instance => Get.find();
|
static IdCardVerificationController get instance => Get.find();
|
||||||
|
|
||||||
// Static form key
|
// Services
|
||||||
// final GlobalKey<FormState> formKey = TGlobalFormKey.idCardVerification();
|
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
|
// Using AWS for face recognition
|
||||||
|
final AwsRecognitionService _faceService = AwsRecognitionService.instance;
|
||||||
|
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
|
|
||||||
// Maximum allowed file size in bytes (4MB)
|
// Maximum allowed file size in bytes (4MB)
|
||||||
|
@ -42,7 +46,10 @@ class IdCardVerificationController extends GetxController {
|
||||||
final Rx<KtpModel?> ktpModel = Rx<KtpModel?>(null);
|
final Rx<KtpModel?> ktpModel = Rx<KtpModel?>(null);
|
||||||
final Rx<KtaModel?> ktaModel = Rx<KtaModel?>(null);
|
final Rx<KtaModel?> ktaModel = Rx<KtaModel?>(null);
|
||||||
|
|
||||||
// Store face ID from the ID card for later comparison
|
// Use FaceModel to store face details from ID card
|
||||||
|
final Rx<FaceModel> idCardFace = Rx<FaceModel>(FaceModel.empty());
|
||||||
|
|
||||||
|
// Store face ID from the ID card for later comparison (for backward compatibility)
|
||||||
final RxString idCardFaceId = RxString('');
|
final RxString idCardFaceId = RxString('');
|
||||||
final RxBool hasFaceDetected = RxBool(false);
|
final RxBool hasFaceDetected = RxBool(false);
|
||||||
|
|
||||||
|
@ -123,7 +130,8 @@ class IdCardVerificationController extends GetxController {
|
||||||
ktpModel.value = null;
|
ktpModel.value = null;
|
||||||
ktaModel.value = null;
|
ktaModel.value = null;
|
||||||
|
|
||||||
// Reset face detection flags
|
// Reset face detection data
|
||||||
|
idCardFace.value = FaceModel.empty();
|
||||||
idCardFaceId.value = '';
|
idCardFaceId.value = '';
|
||||||
hasFaceDetected.value = false;
|
hasFaceDetected.value = false;
|
||||||
|
|
||||||
|
@ -141,7 +149,7 @@ class IdCardVerificationController extends GetxController {
|
||||||
bool isImageValid = false;
|
bool isImageValid = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to process the ID card to check if it can be processed properly
|
// Process the ID card with OCR (still using Azure OCR)
|
||||||
final result = await _ocrService.processIdCard(
|
final result = await _ocrService.processIdCard(
|
||||||
idCardImage.value!,
|
idCardImage.value!,
|
||||||
isOfficer,
|
isOfficer,
|
||||||
|
@ -165,16 +173,19 @@ class IdCardVerificationController extends GetxController {
|
||||||
ktpModel.value = _ocrService.createKtpModel(result);
|
ktpModel.value = _ocrService.createKtpModel(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to detect faces in the ID card image
|
// Try to detect faces in the ID card image using AWS Rekognition
|
||||||
if (isImageValid) {
|
if (isImageValid) {
|
||||||
try {
|
try {
|
||||||
final faces = await _ocrService.detectFacesInImage(
|
// Use AWS Rekognition to detect faces
|
||||||
idCardImage.value!,
|
final faces = await _faceService.detectFaces(idCardImage.value!);
|
||||||
);
|
|
||||||
if (faces.isNotEmpty) {
|
if (faces.isNotEmpty) {
|
||||||
idCardFaceId.value = faces[0]['faceId'] ?? '';
|
// Store the face model
|
||||||
hasFaceDetected.value = idCardFaceId.value.isNotEmpty;
|
idCardFace.value = faces.first;
|
||||||
print('Face detected in ID card: ${idCardFaceId.value}');
|
|
||||||
|
// For backward compatibility
|
||||||
|
idCardFaceId.value = faces.first.faceId;
|
||||||
|
hasFaceDetected.value = idCardFace.value.hasValidFace;
|
||||||
|
print('Face detected in ID card: ${idCardFace.value.faceId}');
|
||||||
}
|
}
|
||||||
} catch (faceError) {
|
} catch (faceError) {
|
||||||
print('Face detection failed: $faceError');
|
print('Face detection failed: $faceError');
|
||||||
|
@ -231,8 +242,7 @@ class IdCardVerificationController extends GetxController {
|
||||||
String? get idCardImagePath => idCardImage.value?.path;
|
String? get idCardImagePath => idCardImage.value?.path;
|
||||||
|
|
||||||
// Check if the ID card has a detected face
|
// Check if the ID card has a detected face
|
||||||
bool get hasDetectedFace =>
|
bool get hasDetectedFace => idCardFace.value.hasValidFace;
|
||||||
hasFaceDetected.value && idCardFaceId.value.isNotEmpty;
|
|
||||||
|
|
||||||
// Clear ID Card Image
|
// Clear ID Card Image
|
||||||
void clearIdCardImage() {
|
void clearIdCardImage() {
|
||||||
|
@ -245,6 +255,9 @@ class IdCardVerificationController extends GetxController {
|
||||||
hasExtractedInfo.value = false;
|
hasExtractedInfo.value = false;
|
||||||
ktpModel.value = null;
|
ktpModel.value = null;
|
||||||
ktaModel.value = null;
|
ktaModel.value = null;
|
||||||
|
idCardFace.value = FaceModel.empty();
|
||||||
|
idCardFaceId.value = '';
|
||||||
|
hasFaceDetected.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm ID Card Image
|
// Confirm ID Card Image
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/cores/services/aws_rekognition_service.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/selfie_verification_controller.dart';
|
||||||
// ... other imports
|
|
||||||
|
|
||||||
class IdentityVerificationController extends GetxController {
|
class IdentityVerificationController extends GetxController {
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
@ -13,6 +16,8 @@ class IdentityVerificationController extends GetxController {
|
||||||
// Dependencies
|
// Dependencies
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
|
// Use AWS Rekognition for face detection instead of Azure Face API
|
||||||
|
final AwsRecognitionService _faceService = AwsRecognitionService.instance;
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final TextEditingController nikController = TextEditingController();
|
final TextEditingController nikController = TextEditingController();
|
||||||
|
@ -39,27 +44,45 @@ class IdentityVerificationController extends GetxController {
|
||||||
final RxBool isFaceVerified = RxBool(false);
|
final RxBool isFaceVerified = RxBool(false);
|
||||||
final RxString faceVerificationMessage = RxString('');
|
final RxString faceVerificationMessage = RxString('');
|
||||||
|
|
||||||
|
// Use FaceComparisonResult for face verification
|
||||||
|
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||||
|
Rx<FaceComparisonResult?>(null);
|
||||||
|
|
||||||
// Gender selection
|
// Gender selection
|
||||||
final Rx<String?> selectedGender = Rx<String?>(null);
|
final Rx<String?> selectedGender = Rx<String?>(null);
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
final RxBool isFormValid = RxBool(true);
|
final RxBool isFormValid = RxBool(true);
|
||||||
|
|
||||||
IdentityVerificationController({required this.isOfficer}) {
|
// Flag to prevent infinite loop
|
||||||
// Apply data from previous step if available
|
bool _isApplyingData = false;
|
||||||
_applyIdCardData();
|
|
||||||
}
|
IdentityVerificationController({required this.isOfficer});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_applyIdCardData();
|
// Delay data application to avoid initialization issues
|
||||||
|
Future.microtask(() => _safeApplyIdCardData());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply ID card data from the previous step
|
// Safely apply ID card data without risking stack overflow
|
||||||
void _applyIdCardData() {
|
void _safeApplyIdCardData() {
|
||||||
|
if (_isApplyingData) return; // Guard against recursive calls
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_isApplyingData = true;
|
||||||
|
|
||||||
|
// Check if FormRegistrationController is ready
|
||||||
|
if (!Get.isRegistered<FormRegistrationController>()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final formController = Get.find<FormRegistrationController>();
|
final formController = Get.find<FormRegistrationController>();
|
||||||
|
if (formController.idCardData.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final idCardData = formController.idCardData.value;
|
final idCardData = formController.idCardData.value;
|
||||||
|
|
||||||
if (idCardData != null) {
|
if (idCardData != null) {
|
||||||
|
@ -104,8 +127,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
KtaModel ktaModel = idCardData;
|
KtaModel ktaModel = idCardData;
|
||||||
|
|
||||||
// For officer, we'd fill in different fields as needed
|
// For officer, we'd fill in different fields as needed
|
||||||
// Since we don't require NIK for officers, no need to set nikController
|
|
||||||
|
|
||||||
if (ktaModel.name.isNotEmpty) {
|
if (ktaModel.name.isNotEmpty) {
|
||||||
fullNameController.text = ktaModel.name;
|
fullNameController.text = ktaModel.name;
|
||||||
}
|
}
|
||||||
|
@ -124,6 +145,8 @@ class IdentityVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error applying ID card data: $e');
|
print('Error applying ID card data: $e');
|
||||||
|
} finally {
|
||||||
|
_isApplyingData = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,33 +295,46 @@ class IdentityVerificationController extends GetxController {
|
||||||
return matches >= (parts1.length / 2).floor();
|
return matches >= (parts1.length / 2).floor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple face verification function simulation
|
// Face verification function using AWS Rekognition instead of Azure
|
||||||
void verifyFaceMatch() {
|
void verifyFaceMatch() {
|
||||||
isVerifyingFace.value = true;
|
isVerifyingFace.value = true;
|
||||||
|
|
||||||
// Simulate verification process with a delay
|
// Get ID card and selfie images
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
|
||||||
try {
|
|
||||||
// In a real implementation, this would call the proper face verification API
|
|
||||||
final formController = Get.find<FormRegistrationController>();
|
final formController = Get.find<FormRegistrationController>();
|
||||||
final idCardData = formController.idCardData.value;
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
if (idCardData != null) {
|
// Check if we have both images
|
||||||
// Simulate successful match for demonstration
|
if (idCardController.idCardImage.value == null ||
|
||||||
isFaceVerified.value = true;
|
selfieController.selfieImage.value == null) {
|
||||||
faceVerificationMessage.value = 'Face verification successful!';
|
|
||||||
} else {
|
|
||||||
isFaceVerified.value = false;
|
isFaceVerified.value = false;
|
||||||
faceVerificationMessage.value =
|
faceVerificationMessage.value =
|
||||||
'No ID card data available to verify face.';
|
'Both ID card and selfie are required for face verification.';
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
isFaceVerified.value = false;
|
|
||||||
faceVerificationMessage.value = 'Error during face verification.';
|
|
||||||
print('Face verification error: $e');
|
|
||||||
} finally {
|
|
||||||
isVerifyingFace.value = false;
|
isVerifyingFace.value = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use AWS Rekognition to compare faces
|
||||||
|
_faceService
|
||||||
|
.compareFaces(
|
||||||
|
idCardController.idCardImage.value!,
|
||||||
|
selfieController.selfieImage.value!,
|
||||||
|
)
|
||||||
|
.then((result) {
|
||||||
|
// Store the comparison result
|
||||||
|
faceComparisonResult.value = result;
|
||||||
|
|
||||||
|
// Update verification status
|
||||||
|
isFaceVerified.value = result.isMatch;
|
||||||
|
faceVerificationMessage.value = result.message;
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
isFaceVerified.value = false;
|
||||||
|
faceVerificationMessage.value = 'Error during face verification: $e';
|
||||||
|
print('Face verification error: $e');
|
||||||
|
})
|
||||||
|
.whenComplete(() {
|
||||||
|
isVerifyingFace.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,16 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/aws_rekognition_service.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
||||||
|
|
||||||
class SelfieVerificationController extends GetxController {
|
class SelfieVerificationController extends GetxController {
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
static SelfieVerificationController get instance => Get.find();
|
static SelfieVerificationController get instance => Get.find();
|
||||||
|
|
||||||
// Static form key
|
// Services - Use AWS Rekognition
|
||||||
// final GlobalKey<FormState> formKey = TGlobalFormKey.selfieVerification();
|
final AwsRecognitionService _faceService = AwsRecognitionService.instance;
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
|
||||||
|
|
||||||
// Maximum allowed file size in bytes (4MB)
|
// Maximum allowed file size in bytes (4MB)
|
||||||
final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes
|
final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes
|
||||||
|
@ -36,6 +36,15 @@ class SelfieVerificationController extends GetxController {
|
||||||
|
|
||||||
// Face comparison with ID card photo
|
// Face comparison with ID card photo
|
||||||
final RxBool isComparingWithIDCard = RxBool(false);
|
final RxBool isComparingWithIDCard = RxBool(false);
|
||||||
|
|
||||||
|
// Use FaceModel to store selfie face details
|
||||||
|
final Rx<FaceModel> selfieFace = Rx<FaceModel>(FaceModel.empty());
|
||||||
|
|
||||||
|
// Use FaceComparisonResult to store comparison results
|
||||||
|
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||||
|
Rx<FaceComparisonResult?>(null);
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
final RxBool isMatchWithIDCard = RxBool(false);
|
final RxBool isMatchWithIDCard = RxBool(false);
|
||||||
final RxDouble matchConfidence = RxDouble(0.0);
|
final RxDouble matchConfidence = RxDouble(0.0);
|
||||||
final RxString selfieImageFaceId = RxString('');
|
final RxString selfieImageFaceId = RxString('');
|
||||||
|
@ -71,9 +80,13 @@ class SelfieVerificationController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isUploadingSelfie.value = true;
|
isUploadingSelfie.value = true;
|
||||||
hasConfirmedSelfie.value = false; // Reset confirmation when image changes
|
hasConfirmedSelfie.value = false; // Reset confirmation when image changes
|
||||||
isMatchWithIDCard.value = false; // Reset face match status
|
|
||||||
matchConfidence.value = 0.0; // Reset confidence score
|
// Reset face data
|
||||||
selfieImageFaceId.value = ''; // Reset selfie face ID
|
selfieFace.value = FaceModel.empty();
|
||||||
|
faceComparisonResult.value = null;
|
||||||
|
isMatchWithIDCard.value = false;
|
||||||
|
matchConfidence.value = 0.0;
|
||||||
|
selfieImageFaceId.value = '';
|
||||||
|
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
final XFile? image = await picker.pickImage(
|
final XFile? image = await picker.pickImage(
|
||||||
|
@ -110,7 +123,7 @@ class SelfieVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial validation of selfie image
|
// Initial validation of selfie image using AWS Rekognition
|
||||||
Future<void> validateSelfieImage() async {
|
Future<void> validateSelfieImage() async {
|
||||||
// Clear previous validation messages
|
// Clear previous validation messages
|
||||||
clearErrors();
|
clearErrors();
|
||||||
|
@ -124,36 +137,29 @@ class SelfieVerificationController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isVerifyingFace.value = true;
|
isVerifyingFace.value = true;
|
||||||
|
|
||||||
// Use liveness check to validate if the selfie has a valid face
|
// Use AWS Rekognition for liveness check
|
||||||
final livenessResult = await _ocrService.performLivenessCheck(
|
final FaceModel livenessFace = await _faceService.performLivenessCheck(
|
||||||
selfieImage.value!,
|
selfieImage.value!,
|
||||||
);
|
);
|
||||||
isLivenessCheckPassed.value = livenessResult['isLive'] ?? false;
|
|
||||||
|
|
||||||
if (livenessResult['isLive'] == true) {
|
// Update the face model
|
||||||
|
selfieFace.value = livenessFace;
|
||||||
|
|
||||||
|
// Update liveness status
|
||||||
|
isLivenessCheckPassed.value = livenessFace.isLive;
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
|
selfieImageFaceId.value = livenessFace.faceId;
|
||||||
|
|
||||||
|
if (livenessFace.isLive) {
|
||||||
isSelfieValid.value = true;
|
isSelfieValid.value = true;
|
||||||
selfieValidationMessage.value =
|
selfieValidationMessage.value = livenessFace.message;
|
||||||
'Face detected. Please confirm this is you.';
|
|
||||||
|
|
||||||
// Try to detect face and get face ID for later comparison
|
|
||||||
try {
|
|
||||||
final faces = await _ocrService.detectFacesInImage(
|
|
||||||
selfieImage.value!,
|
|
||||||
);
|
|
||||||
if (faces.isNotEmpty) {
|
|
||||||
selfieImageFaceId.value = faces[0]['faceId'] ?? '';
|
|
||||||
|
|
||||||
// Compare with ID card photo if available
|
// Compare with ID card photo if available
|
||||||
await compareWithIDCardPhoto();
|
await compareWithIDCardPhoto();
|
||||||
}
|
|
||||||
} catch (faceError) {
|
|
||||||
print('Selfie face detection failed: $faceError');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
isSelfieValid.value = false;
|
isSelfieValid.value = false;
|
||||||
selfieValidationMessage.value =
|
selfieValidationMessage.value = livenessFace.message;
|
||||||
livenessResult['message'] ??
|
|
||||||
'No face detected or liveness check failed. Please take a clearer selfie.';
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isSelfieValid.value = false;
|
isSelfieValid.value = false;
|
||||||
|
@ -163,45 +169,44 @@ class SelfieVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare selfie with ID card photo
|
// Compare selfie with ID card photo using AWS Rekognition
|
||||||
Future<void> compareWithIDCardPhoto() async {
|
Future<void> compareWithIDCardPhoto() async {
|
||||||
try {
|
try {
|
||||||
final idCardController = Get.find<IdCardVerificationController>();
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
|
||||||
// Check if both face IDs are available
|
// Check if both images are available
|
||||||
if (selfieImageFaceId.value.isEmpty ||
|
if (selfieImage.value == null ||
|
||||||
!idCardController.hasDetectedFace) {
|
idCardController.idCardImage.value == null) {
|
||||||
print('Cannot compare faces: Missing face ID');
|
print('Cannot compare faces: Missing images');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isComparingWithIDCard.value = true;
|
isComparingWithIDCard.value = true;
|
||||||
|
|
||||||
// Compare the two faces
|
// Use AWS Rekognition to compare the faces
|
||||||
final result = await _ocrService.compareFaceIds(
|
final comparisonResult = await _faceService.compareFaces(
|
||||||
idCardController.idCardFaceId.value,
|
idCardController.idCardImage.value!,
|
||||||
selfieImageFaceId.value,
|
selfieImage.value!
|
||||||
);
|
);
|
||||||
|
|
||||||
isMatchWithIDCard.value = result['isMatch'] ?? false;
|
// Store the comparison result
|
||||||
matchConfidence.value = result['confidence'] ?? 0.0;
|
faceComparisonResult.value = comparisonResult;
|
||||||
|
|
||||||
|
// For backward compatibility
|
||||||
|
isMatchWithIDCard.value = comparisonResult.isMatch;
|
||||||
|
matchConfidence.value = comparisonResult.confidence;
|
||||||
|
|
||||||
// Update validation message to include face comparison result
|
// Update validation message to include face comparison result
|
||||||
if (isMatchWithIDCard.value) {
|
selfieValidationMessage.value = comparisonResult.message;
|
||||||
selfieValidationMessage.value =
|
|
||||||
'Face verified! Your selfie matches your ID photo with ${(matchConfidence.value * 100).toStringAsFixed(1)}% confidence.';
|
|
||||||
} else if (matchConfidence.value > 0) {
|
|
||||||
selfieValidationMessage.value =
|
|
||||||
'Face verification failed. Your selfie does not match your ID photo (${(matchConfidence.value * 100).toStringAsFixed(1)}% similarity).';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Face comparison error: $e');
|
print('Face comparison error: $e');
|
||||||
|
selfieValidationMessage.value = 'Face comparison error: $e';
|
||||||
} finally {
|
} finally {
|
||||||
isComparingWithIDCard.value = false;
|
isComparingWithIDCard.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually trigger face comparison with ID card
|
// Manually trigger face comparison with ID card using AWS Rekognition
|
||||||
Future<void> verifyFaceMatchWithIDCard() async {
|
Future<void> verifyFaceMatchWithIDCard() async {
|
||||||
if (selfieImage.value == null) {
|
if (selfieImage.value == null) {
|
||||||
selfieError.value = 'Please take a selfie first';
|
selfieError.value = 'Please take a selfie first';
|
||||||
|
@ -214,24 +219,13 @@ class SelfieVerificationController extends GetxController {
|
||||||
// Get the ID card controller
|
// Get the ID card controller
|
||||||
final idCardController = Get.find<IdCardVerificationController>();
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
|
||||||
if (!idCardController.hasDetectedFace) {
|
if (idCardController.idCardImage.value == null) {
|
||||||
selfieValidationMessage.value =
|
selfieValidationMessage.value =
|
||||||
'No face detected in ID card for comparison';
|
'ID card image is required for comparison';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a selfie face ID yet, detect it now
|
// Compare faces directly using AWS Rekognition
|
||||||
if (selfieImageFaceId.value.isEmpty) {
|
|
||||||
final faces = await _ocrService.detectFacesInImage(selfieImage.value!);
|
|
||||||
if (faces.isNotEmpty) {
|
|
||||||
selfieImageFaceId.value = faces[0]['faceId'] ?? '';
|
|
||||||
} else {
|
|
||||||
selfieValidationMessage.value = 'No face detected in your selfie';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare faces
|
|
||||||
await compareWithIDCardPhoto();
|
await compareWithIDCardPhoto();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
selfieValidationMessage.value = 'Face verification failed: $e';
|
selfieValidationMessage.value = 'Face verification failed: $e';
|
||||||
|
@ -248,6 +242,8 @@ class SelfieVerificationController extends GetxController {
|
||||||
selfieValidationMessage.value = '';
|
selfieValidationMessage.value = '';
|
||||||
isLivenessCheckPassed.value = false;
|
isLivenessCheckPassed.value = false;
|
||||||
hasConfirmedSelfie.value = false;
|
hasConfirmedSelfie.value = false;
|
||||||
|
selfieFace.value = FaceModel.empty();
|
||||||
|
faceComparisonResult.value = null;
|
||||||
isMatchWithIDCard.value = false;
|
isMatchWithIDCard.value = false;
|
||||||
matchConfidence.value = 0.0;
|
matchConfidence.value = 0.0;
|
||||||
selfieImageFaceId.value = '';
|
selfieImageFaceId.value = '';
|
||||||
|
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart';
|
import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart';
|
||||||
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
||||||
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/cores/services/location_service.dart';
|
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/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
|
|
||||||
class PanicButtonRepository extends GetxController {
|
class PanicButtonRepository extends GetxController {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export 'models/permissions_model.dart';
|
export '../../../auth/data/models/permissions_model.dart';
|
||||||
export 'models/profile_model.dart';
|
export 'models/profile_model.dart';
|
||||||
export 'models/resources_model.dart';
|
export '../../../auth/data/models/resources_model.dart';
|
||||||
export 'models/roles_model.dart';
|
export 'models/roles_model.dart';
|
||||||
export 'models/users_model.dart';
|
export 'models/users_model.dart';
|
||||||
export 'supadart-models/permissions_model_supadart.dart';
|
export 'supadart-models/permissions_model_supadart.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/Logger.dart';
|
import 'package:logger/Logger.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/permissions_model.dart';
|
import 'package:sigap/src/features/auth/data/models/permissions_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/resources_model.dart';
|
import 'package:sigap/src/features/auth/data/models/resources_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/Logger.dart';
|
import 'package:logger/Logger.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/permissions_model.dart';
|
import 'package:sigap/src/features/auth/data/models/permissions_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
class Endpoints {
|
class Endpoints {
|
||||||
|
// Environment variables
|
||||||
|
static String get supabaseUrl => dotenv.env['SUPABASE_URL'] ?? '';
|
||||||
|
static String get supabaseAnonKey => dotenv.env['SUPABASE_ANON_KEY'] ?? '';
|
||||||
|
static String get supabaseServiceRoleKey =>
|
||||||
|
dotenv.env['SERVICE_ROLE_SECRET'] ?? ' ';
|
||||||
|
|
||||||
// Base URL
|
// Base URL
|
||||||
static const devUrl = "";
|
static const devUrl = "";
|
||||||
static const String prodUrl = '';
|
static const String prodUrl = '';
|
||||||
|
|
||||||
static const String baseUrl = '$devUrl/api';
|
static const String baseUrl = '$devUrl/api';
|
||||||
|
|
||||||
|
|
||||||
static String get azureResource => dotenv.env['AZURE_RESOURCE_NAME'] ?? '';
|
static String get azureResource => dotenv.env['AZURE_RESOURCE_NAME'] ?? '';
|
||||||
static String get azureFaceResource =>
|
static String get azureFaceResource =>
|
||||||
dotenv.env['AZURE_FACE_RESOURCE_NAME'] ?? '';
|
dotenv.env['AZURE_FACE_RESOURCE_NAME'] ?? '';
|
||||||
|
@ -26,4 +32,16 @@ class Endpoints {
|
||||||
|
|
||||||
static String get faceApiPath => 'face/v1.2/detect';
|
static String get faceApiPath => 'face/v1.2/detect';
|
||||||
static String get faceVerifyPath => 'face/v1.2/verify';
|
static String get faceVerifyPath => 'face/v1.2/verify';
|
||||||
|
|
||||||
|
// AWS Recognition Configuration
|
||||||
|
static String awsRegion =
|
||||||
|
dotenv.env['AWS_REGION'] ?? ''; // Replace with your AWS region
|
||||||
|
static String awsAccessKey = dotenv.env['AWS_ACCESS_KEY'] ?? '';
|
||||||
|
static String awsSecretKey = dotenv.env['AWS_SECRET_KEY'] ?? '';
|
||||||
|
static String awsRekognitionEndpoint =
|
||||||
|
'https://rekognition.$awsRegion.amazonaws.com';
|
||||||
|
|
||||||
|
// Supabase Edge Functions
|
||||||
|
static String get detectFace => '$supabaseUrl/function/v1/detect-face';
|
||||||
|
static String get verifyFace => '$supabaseUrl/function/v1/verify-face';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class AwsSignature {
|
||||||
|
final String accessKey;
|
||||||
|
final String secretKey;
|
||||||
|
final String region;
|
||||||
|
final String serviceName;
|
||||||
|
|
||||||
|
AwsSignature({
|
||||||
|
required this.accessKey,
|
||||||
|
required this.secretKey,
|
||||||
|
required this.region,
|
||||||
|
required this.serviceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, String> buildRequestHeaders({
|
||||||
|
required String method,
|
||||||
|
required Uri uri,
|
||||||
|
required Map<String, dynamic> payload,
|
||||||
|
required DateTime dateTime,
|
||||||
|
}) {
|
||||||
|
// Format date strings for AWS signature
|
||||||
|
final amzDate = _formatAmzDate(dateTime);
|
||||||
|
final dateStamp = _formatDateStamp(dateTime);
|
||||||
|
|
||||||
|
// Create canonical request components
|
||||||
|
final canonicalUri = uri.path.isEmpty ? '/' : uri.path;
|
||||||
|
final canonicalQueryString = _buildCanonicalQueryString(uri);
|
||||||
|
final payloadJson = jsonEncode(payload);
|
||||||
|
final payloadHash = _hashString(payloadJson);
|
||||||
|
|
||||||
|
// Default headers for AWS Recognition
|
||||||
|
final headers = {
|
||||||
|
'content-type': 'application/x-amz-json-1.1',
|
||||||
|
'host': uri.host,
|
||||||
|
'x-amz-date': amzDate,
|
||||||
|
'x-amz-target': 'RekognitionService.$method',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create canonical headers string
|
||||||
|
final canonicalHeaders = _buildCanonicalHeaders(headers);
|
||||||
|
final signedHeaders = _buildSignedHeaders(headers);
|
||||||
|
|
||||||
|
// Build canonical request
|
||||||
|
final canonicalRequest = [
|
||||||
|
method,
|
||||||
|
canonicalUri,
|
||||||
|
canonicalQueryString,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
payloadHash,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Create string to sign
|
||||||
|
final algorithm = 'AWS4-HMAC-SHA256';
|
||||||
|
final credentialScope = '$dateStamp/$region/$serviceName/aws4_request';
|
||||||
|
final stringToSign = [
|
||||||
|
algorithm,
|
||||||
|
amzDate,
|
||||||
|
credentialScope,
|
||||||
|
_hashString(canonicalRequest),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Calculate signature
|
||||||
|
final signature = _calculateSignature(
|
||||||
|
secretKey: secretKey,
|
||||||
|
dateStamp: dateStamp,
|
||||||
|
region: region,
|
||||||
|
serviceName: serviceName,
|
||||||
|
stringToSign: stringToSign,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create Authorization header
|
||||||
|
final authorizationHeader = [
|
||||||
|
'$algorithm Credential=$accessKey/$credentialScope',
|
||||||
|
'SignedHeaders=$signedHeaders',
|
||||||
|
'Signature=$signature',
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
// Return final headers
|
||||||
|
return {...headers, 'Authorization': authorizationHeader};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatAmzDate(DateTime dateTime) {
|
||||||
|
return DateFormat("yyyyMMdd'T'HHmmss'Z'").format(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDateStamp(DateTime dateTime) {
|
||||||
|
return DateFormat('yyyyMMdd').format(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildCanonicalQueryString(Uri uri) {
|
||||||
|
if (uri.queryParameters.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortedParameters = uri.queryParameters.keys.toList()..sort();
|
||||||
|
return sortedParameters
|
||||||
|
.map((key) => '$key=${Uri.encodeComponent(uri.queryParameters[key]!)}')
|
||||||
|
.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildCanonicalHeaders(Map<String, String> headers) {
|
||||||
|
final sortedKeys =
|
||||||
|
headers.keys.map((k) => k.toLowerCase()).toList()..sort();
|
||||||
|
return sortedKeys.map((key) => '$key:${headers[key]?.trim()}\n').join();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildSignedHeaders(Map<String, String> headers) {
|
||||||
|
final sortedKeys =
|
||||||
|
headers.keys.map((k) => k.toLowerCase()).toList()..sort();
|
||||||
|
return sortedKeys.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _hashString(String input) {
|
||||||
|
final bytes = utf8.encode(input);
|
||||||
|
final digest = sha256.convert(bytes);
|
||||||
|
return digest.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _calculateSignature({
|
||||||
|
required String secretKey,
|
||||||
|
required String dateStamp,
|
||||||
|
required String region,
|
||||||
|
required String serviceName,
|
||||||
|
required String stringToSign,
|
||||||
|
}) {
|
||||||
|
// Create signing key
|
||||||
|
final kDate = _hmacSha256('AWS4$secretKey', dateStamp);
|
||||||
|
final kRegion = _hmacSha256(kDate, region);
|
||||||
|
final kService = _hmacSha256(kRegion, serviceName);
|
||||||
|
final kSigning = _hmacSha256(kService, 'aws4_request');
|
||||||
|
|
||||||
|
// Calculate signature
|
||||||
|
return _hmacSha256Hex(kSigning, stringToSign);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> _hmacSha256(dynamic key, String message) {
|
||||||
|
var hmac = Hmac(sha256, key is String ? utf8.encode(key) : key);
|
||||||
|
var digest = hmac.convert(utf8.encode(message));
|
||||||
|
return digest.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _hmacSha256Hex(List<int> key, String message) {
|
||||||
|
var hmac = Hmac(sha256, key);
|
||||||
|
var digest = hmac.convert(utf8.encode(message));
|
||||||
|
return digest.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,7 +77,8 @@
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3",
|
||||||
|
"@aws-sdk/client-rekognition": "^3.449.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snaplet/copycat": "^6.0.0",
|
"@snaplet/copycat": "^6.0.0",
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||||
|
// https://deno.land/manual/getting_started/setup_your_environment
|
||||||
|
// This enables autocomplete, go to definition, etc.
|
||||||
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
||||||
|
const AWS_REGION = Deno.env.get('AWS_REGION');
|
||||||
|
const AWS_ACCESS_KEY = Deno.env.get('AWS_ACCESS_KEY');
|
||||||
|
const AWS_SECRET_KEY = Deno.env.get('AWS_SECRET_KEY');
|
||||||
|
serve(async (req)=>{
|
||||||
|
try {
|
||||||
|
// Check if we have AWS credentials
|
||||||
|
if (!AWS_REGION || !AWS_ACCESS_KEY || !AWS_SECRET_KEY) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: 'AWS credentials are not configured'
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Parse the multipart form data to get the image
|
||||||
|
const formData = await req.formData();
|
||||||
|
const image = formData.get('image');
|
||||||
|
if (!image || !(image instanceof File)) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: 'Image file is required'
|
||||||
|
}), {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Convert image to base64
|
||||||
|
const imageBuffer = await image.arrayBuffer();
|
||||||
|
const base64Image = btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
|
||||||
|
// Create AWS signature for authorization
|
||||||
|
const date = new Date();
|
||||||
|
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
||||||
|
const dateStamp = amzDate.substring(0, 8);
|
||||||
|
const host = `rekognition.${AWS_REGION}.amazonaws.com`;
|
||||||
|
const endpoint = `https://${host}/`;
|
||||||
|
const request = {
|
||||||
|
"Image": {
|
||||||
|
"Bytes": base64Image
|
||||||
|
},
|
||||||
|
"Attributes": [
|
||||||
|
"ALL"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// AWS Signature V4 calculation
|
||||||
|
const method = 'POST';
|
||||||
|
const service = 'rekognition';
|
||||||
|
const contentType = 'application/x-amz-json-1.1';
|
||||||
|
const amzTarget = 'RekognitionService.DetectFaces';
|
||||||
|
const canonicalUri = '/';
|
||||||
|
const canonicalQueryString = '';
|
||||||
|
const payloadHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(request))).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''));
|
||||||
|
const canonicalHeaders = `content-type:${contentType}\n` + `host:${host}\n` + `x-amz-date:${amzDate}\n` + `x-amz-target:${amzTarget}\n`;
|
||||||
|
const signedHeaders = 'content-type;host;x-amz-date;x-amz-target';
|
||||||
|
const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
||||||
|
const algorithm = 'AWS4-HMAC-SHA256';
|
||||||
|
const credentialScope = `${dateStamp}/${AWS_REGION}/${service}/aws4_request`;
|
||||||
|
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalRequest)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''))}`;
|
||||||
|
const getSignatureKey = async (key, dateStamp, regionName, serviceName)=>{
|
||||||
|
const kDate = await crypto.subtle.importKey("raw", new TextEncoder().encode(`AWS4${key}`), {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
const kRegion = await crypto.subtle.sign("HMAC", kDate, new TextEncoder().encode(regionName));
|
||||||
|
const kService = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kRegion, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode(serviceName));
|
||||||
|
return crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kService, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode("aws4_request"));
|
||||||
|
};
|
||||||
|
const signingKey = await getSignatureKey(AWS_SECRET_KEY, dateStamp, AWS_REGION, service);
|
||||||
|
const signature = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", signingKey, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode(stringToSign)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''));
|
||||||
|
const authHeader = `${algorithm} ` + `Credential=${AWS_ACCESS_KEY}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`;
|
||||||
|
// Make request to AWS Rekognition
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'X-Amz-Date': amzDate,
|
||||||
|
'X-Amz-Target': amzTarget,
|
||||||
|
'Authorization': authHeader
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
faceDetails: data.FaceDetails || [],
|
||||||
|
count: (data.FaceDetails || []).length
|
||||||
|
}), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in detect-face function:", error);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: "Failed to process the image",
|
||||||
|
details: error instanceof Error ? error.message : String(error)
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}); /* To invoke locally:
|
||||||
|
|
||||||
|
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
|
||||||
|
2. Make an HTTP request:
|
||||||
|
|
||||||
|
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/detect-face' \
|
||||||
|
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{"name":"Functions"}'
|
||||||
|
|
||||||
|
*/
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY');
|
||||||
|
Deno.serve(async (req)=>{
|
||||||
|
const { to, subject, html } = await req.json();
|
||||||
|
const res = await fetch('https://api.resend.com/emails', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${RESEND_API_KEY}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
from: 'sigap-support@gmail.com',
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
html
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,151 @@
|
||||||
|
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||||
|
// https://deno.land/manual/getting_started/setup_your_environment
|
||||||
|
// This enables autocomplete, go to definition, etc.
|
||||||
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
|
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
||||||
|
const AWS_REGION = Deno.env.get('AWS_REGION');
|
||||||
|
const AWS_ACCESS_KEY = Deno.env.get('AWS_ACCESS_KEY');
|
||||||
|
const AWS_SECRET_KEY = Deno.env.get('AWS_SECRET_KEY');
|
||||||
|
serve(async (req)=>{
|
||||||
|
try {
|
||||||
|
// Check if we have AWS credentials
|
||||||
|
if (!AWS_REGION || !AWS_ACCESS_KEY || !AWS_SECRET_KEY) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: 'AWS credentials are not configured'
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Parse the multipart form data to get the images
|
||||||
|
const formData = await req.formData();
|
||||||
|
const ktpImage = formData.get('ktp');
|
||||||
|
const selfieImage = formData.get('selfie');
|
||||||
|
if (!ktpImage || !(ktpImage instanceof File) || !selfieImage || !(selfieImage instanceof File)) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: 'Both KTP and selfie images are required'
|
||||||
|
}), {
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Convert images to base64
|
||||||
|
const ktpBuffer = await ktpImage.arrayBuffer();
|
||||||
|
const selfieBuffer = await selfieImage.arrayBuffer();
|
||||||
|
const ktpBase64 = btoa(String.fromCharCode(...new Uint8Array(ktpBuffer)));
|
||||||
|
const selfieBase64 = btoa(String.fromCharCode(...new Uint8Array(selfieBuffer)));
|
||||||
|
// Create AWS signature for authorization
|
||||||
|
const date = new Date();
|
||||||
|
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
||||||
|
const dateStamp = amzDate.substring(0, 8);
|
||||||
|
const host = `rekognition.${AWS_REGION}.amazonaws.com`;
|
||||||
|
const endpoint = `https://${host}/`;
|
||||||
|
const request = {
|
||||||
|
"SourceImage": {
|
||||||
|
"Bytes": ktpBase64
|
||||||
|
},
|
||||||
|
"TargetImage": {
|
||||||
|
"Bytes": selfieBase64
|
||||||
|
},
|
||||||
|
"SimilarityThreshold": 70
|
||||||
|
};
|
||||||
|
// AWS Signature V4 calculation
|
||||||
|
const method = 'POST';
|
||||||
|
const service = 'rekognition';
|
||||||
|
const contentType = 'application/x-amz-json-1.1';
|
||||||
|
const amzTarget = 'RekognitionService.CompareFaces';
|
||||||
|
const canonicalUri = '/';
|
||||||
|
const canonicalQueryString = '';
|
||||||
|
const payloadHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(request))).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''));
|
||||||
|
const canonicalHeaders = `content-type:${contentType}\n` + `host:${host}\n` + `x-amz-date:${amzDate}\n` + `x-amz-target:${amzTarget}\n`;
|
||||||
|
const signedHeaders = 'content-type;host;x-amz-date;x-amz-target';
|
||||||
|
const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
||||||
|
const algorithm = 'AWS4-HMAC-SHA256';
|
||||||
|
const credentialScope = `${dateStamp}/${AWS_REGION}/${service}/aws4_request`;
|
||||||
|
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalRequest)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''))}`;
|
||||||
|
const getSignatureKey = async (key, dateStamp, regionName, serviceName)=>{
|
||||||
|
const kDate = await crypto.subtle.importKey("raw", new TextEncoder().encode(`AWS4${key}`), {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]);
|
||||||
|
const kRegion = await crypto.subtle.sign("HMAC", kDate, new TextEncoder().encode(regionName));
|
||||||
|
const kService = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kRegion, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode(serviceName));
|
||||||
|
return crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kService, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode("aws4_request"));
|
||||||
|
};
|
||||||
|
const signingKey = await getSignatureKey(AWS_SECRET_KEY, dateStamp, AWS_REGION, service);
|
||||||
|
const signature = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", signingKey, {
|
||||||
|
name: "HMAC",
|
||||||
|
hash: "SHA-256"
|
||||||
|
}, false, [
|
||||||
|
"sign"
|
||||||
|
]), new TextEncoder().encode(stringToSign)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''));
|
||||||
|
const authHeader = `${algorithm} ` + `Credential=${AWS_ACCESS_KEY}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`;
|
||||||
|
// Make request to AWS Rekognition
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'X-Amz-Date': amzDate,
|
||||||
|
'X-Amz-Target': amzTarget,
|
||||||
|
'Authorization': authHeader
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
// Determine if verification passed
|
||||||
|
const matched = !!(data.FaceMatches && data.FaceMatches.length > 0);
|
||||||
|
let highestSimilarity = 0;
|
||||||
|
if (matched && data.FaceMatches && data.FaceMatches.length > 0) {
|
||||||
|
highestSimilarity = Math.max(...data.FaceMatches.map((match)=>match.Similarity || 0));
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
matched: matched,
|
||||||
|
similarity: highestSimilarity,
|
||||||
|
faceMatches: data.FaceMatches || [],
|
||||||
|
unmatchedFaces: data.UnmatchedFaces || []
|
||||||
|
}), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in verify-face function:", error);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
error: "Failed to verify faces",
|
||||||
|
details: error instanceof Error ? error.message : String(error)
|
||||||
|
}), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}); /* To invoke locally:
|
||||||
|
|
||||||
|
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
|
||||||
|
2. Make an HTTP request:
|
||||||
|
|
||||||
|
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/verify-face' \
|
||||||
|
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{"name":"Functions"}'
|
||||||
|
|
||||||
|
*/
|
Loading…
Reference in New Issue