124 lines
4.0 KiB
Dart
124 lines
4.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
import '../models/verification_session.dart';
|
|
import 'supabase_provider.dart';
|
|
|
|
/// Provider for the verification session controller.
|
|
final verificationSessionControllerProvider =
|
|
Provider<VerificationSessionController>((ref) {
|
|
final client = ref.watch(supabaseClientProvider);
|
|
return VerificationSessionController(client);
|
|
});
|
|
|
|
/// Controller for creating, completing, and listening to verification sessions.
|
|
class VerificationSessionController {
|
|
VerificationSessionController(this._client);
|
|
|
|
final SupabaseClient _client;
|
|
|
|
/// Create a new verification session and return it.
|
|
Future<VerificationSession> createSession({
|
|
required String type,
|
|
String? contextId,
|
|
}) async {
|
|
final userId = _client.auth.currentUser!.id;
|
|
final data = await _client
|
|
.from('verification_sessions')
|
|
.insert({'user_id': userId, 'type': type, 'context_id': contextId})
|
|
.select()
|
|
.single();
|
|
return VerificationSession.fromMap(data);
|
|
}
|
|
|
|
/// Fetch a session by ID.
|
|
Future<VerificationSession?> getSession(String sessionId) async {
|
|
final data = await _client
|
|
.from('verification_sessions')
|
|
.select()
|
|
.eq('id', sessionId)
|
|
.maybeSingle();
|
|
return data == null ? null : VerificationSession.fromMap(data);
|
|
}
|
|
|
|
/// Listen for realtime changes on a specific session (used by web to detect
|
|
/// when mobile completes the verification).
|
|
Stream<VerificationSession> watchSession(String sessionId) {
|
|
return _client
|
|
.from('verification_sessions')
|
|
.stream(primaryKey: ['id'])
|
|
.eq('id', sessionId)
|
|
.map(
|
|
(rows) => rows.isEmpty
|
|
? throw Exception('Session not found')
|
|
: VerificationSession.fromMap(rows.first),
|
|
);
|
|
}
|
|
|
|
/// Complete a session: upload the face photo and mark the session as
|
|
/// completed. Called from the mobile verification screen.
|
|
Future<void> completeSession({
|
|
required String sessionId,
|
|
required Uint8List bytes,
|
|
required String fileName,
|
|
}) async {
|
|
final userId = _client.auth.currentUser!.id;
|
|
final ext = fileName.split('.').last.toLowerCase();
|
|
final path = '$userId/$sessionId.$ext';
|
|
|
|
// Upload to face-enrollment bucket (same bucket used for face photos)
|
|
await _client.storage
|
|
.from('face-enrollment')
|
|
.uploadBinary(
|
|
path,
|
|
bytes,
|
|
fileOptions: const FileOptions(upsert: true),
|
|
);
|
|
final url = _client.storage.from('face-enrollment').getPublicUrl(path);
|
|
|
|
// Mark session completed with the image URL
|
|
await _client
|
|
.from('verification_sessions')
|
|
.update({'status': 'completed', 'image_url': url})
|
|
.eq('id', sessionId);
|
|
}
|
|
|
|
/// After a session completes, apply the result based on the session type.
|
|
/// For 'enrollment': update the user's face photo.
|
|
/// For 'verification': update the attendance log.
|
|
Future<void> applySessionResult(VerificationSession session) async {
|
|
if (session.imageUrl == null) return;
|
|
|
|
if (session.type == 'enrollment') {
|
|
// Update profile face photo
|
|
await _client
|
|
.from('profiles')
|
|
.update({
|
|
'face_photo_url': session.imageUrl,
|
|
'face_enrolled_at': DateTime.now().toUtc().toIso8601String(),
|
|
})
|
|
.eq('id', session.userId);
|
|
} else if (session.type == 'verification' && session.contextId != null) {
|
|
// Update attendance log verification status
|
|
await _client
|
|
.from('attendance_logs')
|
|
.update({
|
|
'verification_status': 'verified',
|
|
'verification_photo_url': session.imageUrl,
|
|
})
|
|
.eq('id', session.contextId!);
|
|
}
|
|
}
|
|
|
|
/// Expire a session (called when dialog is closed prematurely).
|
|
Future<void> expireSession(String sessionId) async {
|
|
await _client
|
|
.from('verification_sessions')
|
|
.update({'status': 'expired'})
|
|
.eq('id', sessionId);
|
|
}
|
|
}
|