tasq/lib/providers/profile_provider.dart

164 lines
4.9 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/profile.dart';
import 'auth_provider.dart';
import 'supabase_provider.dart';
import 'stream_recovery.dart';
final currentUserIdProvider = Provider<String?>((ref) {
final authState = ref.watch(authStateChangesProvider);
// Be explicit about loading/error to avoid dynamic dispatch problems.
return authState.when(
data: (state) => state.session?.user.id,
loading: () => ref.watch(sessionProvider)?.user.id,
error: (error, _) => ref.watch(sessionProvider)?.user.id,
);
});
final currentProfileProvider = StreamProvider<Profile?>((ref) {
final userId = ref.watch(currentUserIdProvider);
if (userId == null) {
return const Stream.empty();
}
final client = ref.watch(supabaseClientProvider);
final wrapper = StreamRecoveryWrapper<Profile?>(
stream: client.from('profiles').stream(primaryKey: ['id']).eq('id', userId),
onPollData: () async {
final data = await client
.from('profiles')
.select()
.eq('id', userId)
.maybeSingle();
return data == null ? [] : [Profile.fromMap(data)];
},
fromMap: Profile.fromMap,
);
ref.onDispose(wrapper.dispose);
return wrapper.stream.map((result) {
return result.data.isEmpty ? null : result.data.first;
});
});
final profilesProvider = StreamProvider<List<Profile>>((ref) {
final client = ref.watch(supabaseClientProvider);
final wrapper = StreamRecoveryWrapper<Profile>(
stream: client
.from('profiles')
.stream(primaryKey: ['id'])
.order('full_name'),
onPollData: () async {
final data = await client.from('profiles').select().order('full_name');
return data.map(Profile.fromMap).toList();
},
fromMap: Profile.fromMap,
);
ref.onDispose(wrapper.dispose);
return wrapper.stream.map((result) => result.data);
});
/// Controller for the current user's profile (update full name / password).
final profileControllerProvider = Provider<ProfileController>((ref) {
final client = ref.watch(supabaseClientProvider);
return ProfileController(client);
});
class ProfileController {
ProfileController(this._client);
final SupabaseClient _client;
/// Update the `profiles.full_name` for the given user id.
Future<void> updateFullName({
required String userId,
required String fullName,
}) async {
await _client
.from('profiles')
.update({'full_name': fullName})
.eq('id', userId);
}
/// Update the current user's password (works for OAuth users too).
Future<void> updatePassword(String password) async {
if (password.length < 8) {
throw Exception('Password must be at least 8 characters');
}
await _client.auth.updateUser(UserAttributes(password: password));
}
/// Upload a profile avatar image and update the profile record.
Future<String> uploadAvatar({
required String userId,
required Uint8List bytes,
required String fileName,
}) async {
final ext = fileName.split('.').last.toLowerCase();
final path = '$userId/avatar.$ext';
await _client.storage
.from('avatars')
.uploadBinary(
path,
bytes,
fileOptions: const FileOptions(upsert: true),
);
final url = _client.storage.from('avatars').getPublicUrl(path);
await _client.from('profiles').update({'avatar_url': url}).eq('id', userId);
return url;
}
/// Upload a face enrollment photo and update the profile record.
Future<String> uploadFacePhoto({
required String userId,
required Uint8List bytes,
required String fileName,
}) async {
final ext = fileName.split('.').last.toLowerCase();
final path = '$userId/face.$ext';
await _client.storage
.from('face-enrollment')
.uploadBinary(
path,
bytes,
fileOptions: const FileOptions(upsert: true),
);
final url = _client.storage.from('face-enrollment').getPublicUrl(path);
await _client
.from('profiles')
.update({
'face_photo_url': url,
'face_enrolled_at': DateTime.now().toUtc().toIso8601String(),
})
.eq('id', userId);
return url;
}
/// Download the face enrollment photo bytes for the given user.
/// Uses Supabase authenticated storage API (works with private buckets).
Future<Uint8List?> downloadFacePhoto(String userId) async {
try {
return await _client.storage
.from('face-enrollment')
.download('$userId/face.jpg');
} catch (_) {
return null;
}
}
}
final isAdminProvider = Provider<bool>((ref) {
final profileAsync = ref.watch(currentProfileProvider);
return profileAsync.maybeWhen(
data: (profile) =>
profile?.role == 'admin' || profile?.role == 'programmer',
orElse: () => false,
);
});