tasq/lib/providers/admin_user_provider.dart

178 lines
4.9 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'supabase_provider.dart';
import '../utils/app_time.dart';
/// Admin user query parameters for server-side pagination.
class AdminUserQuery {
/// Creates admin user query parameters.
const AdminUserQuery({
this.offset = 0,
this.limit = 50,
this.searchQuery = '',
});
/// Offset for pagination.
final int offset;
/// Number of items per page (default: 50).
final int limit;
/// Full text search query.
final String searchQuery;
AdminUserQuery copyWith({int? offset, int? limit, String? searchQuery}) {
return AdminUserQuery(
offset: offset ?? this.offset,
limit: limit ?? this.limit,
searchQuery: searchQuery ?? this.searchQuery,
);
}
}
final adminUserQueryProvider = StateProvider<AdminUserQuery>(
(ref) => const AdminUserQuery(),
);
final adminUserControllerProvider = Provider<AdminUserController>((ref) {
final client = ref.watch(supabaseClientProvider);
return AdminUserController(client);
});
class AdminUserStatus {
AdminUserStatus({required this.email, required this.bannedUntil});
final String? email;
final DateTime? bannedUntil;
bool get isLocked {
if (bannedUntil == null) return false;
return bannedUntil!.isAfter(AppTime.now());
}
}
class AdminUserController {
AdminUserController(this._client);
final SupabaseClient _client;
Future<void> updateProfile({
required String userId,
required String fullName,
required String role,
String religion = 'catholic',
}) async {
await _client
.from('profiles')
.update({'full_name': fullName, 'role': role, 'religion': religion})
.eq('id', userId);
}
Future<void> updateRole({
required String userId,
required String role,
}) async {
await _client.from('profiles').update({'role': role}).eq('id', userId);
}
// Centralized helper that calls the admin Edge Function and surfaces
// a clear error for 401/bad_jwt so the UI can react (sign out/reauth).
Future<dynamic> _invokeAdminFunction(
String action,
Map<String, dynamic> payload,
) async {
final accessToken = _client.auth.currentSession?.accessToken;
final response = await _client.functions.invoke(
'admin_user_management',
body: {'action': action, ...payload},
headers: accessToken == null
? null
: {'Authorization': 'Bearer $accessToken'},
);
if (response.status == 401) {
// If the gateway rejects the JWT, proactively clear the local session
// so the app can re-authenticate and obtain a valid Supabase token.
try {
await _client.auth.signOut();
} catch (_) {
// ignore sign-out errors
}
throw Exception(
'Unauthorized: invalid or expired session token (bad_jwt)',
);
}
if (response.status != 200) {
throw Exception(response.data ?? 'Admin request failed');
}
return response.data;
}
Future<void> setPassword({
required String userId,
required String password,
}) async {
if (password.length < 8) {
throw Exception('Password must be at least 8 characters');
}
await _invokeAdminFunction('set_password', {
'userId': userId,
'password': password,
});
}
Future<void> setLock({required String userId, required bool locked}) async {
await _invokeAdminFunction('set_lock', {
'userId': userId,
'locked': locked,
});
}
Future<AdminUserStatus> fetchStatus(String userId) async {
final data = await _invokeAdminFunction('get_user', {'userId': userId});
final user =
(data as Map<String, dynamic>?)?['user'] as Map<String, dynamic>?;
final email = user?['email'] as String?;
DateTime? bannedUntil;
final bannedRaw = user?['banned_until'];
if (bannedRaw is String) {
bannedUntil = DateTime.tryParse(bannedRaw);
} else if (bannedRaw is DateTime) {
bannedUntil = bannedRaw;
}
return AdminUserStatus(
email: email,
bannedUntil: bannedUntil == null ? null : AppTime.toAppTime(bannedUntil),
);
}
Future<List<Map<String, dynamic>>> listUsers(AdminUserQuery q) async {
final data = await _invokeAdminFunction('list_users', {
'offset': q.offset,
'limit': q.limit,
'searchQuery': q.searchQuery,
});
final users = (data is Map && data['users'] is List)
? (data['users'] as List).cast<Map<String, dynamic>>()
: <Map<String, dynamic>>[];
return users;
}
}
final adminUserStatusProvider = FutureProvider.family
.autoDispose<AdminUserStatus, String>((ref, userId) {
return ref.watch(adminUserControllerProvider).fetchStatus(userId);
});
final adminUsersProvider =
FutureProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
final q = ref.watch(adminUserQueryProvider);
final ctrl = ref.watch(adminUserControllerProvider);
return ctrl.listUsers(q);
});