186 lines
5.6 KiB
Dart
186 lines
5.6 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,
|
|
}) async {
|
|
await _client
|
|
.from('profiles')
|
|
.update({'full_name': fullName, 'role': role})
|
|
.eq('id', userId);
|
|
}
|
|
|
|
Future<void> updateRole({
|
|
required String userId,
|
|
required String role,
|
|
}) async {
|
|
await _client.from('profiles').update({'role': role}).eq('id', userId);
|
|
}
|
|
|
|
/// Password administration — forwarded to the admin Edge Function.
|
|
Future<void> setPassword({
|
|
required String userId,
|
|
required String password,
|
|
}) async {
|
|
final payload = {
|
|
'action': 'set_password',
|
|
'userId': userId,
|
|
'password': password,
|
|
};
|
|
final accessToken = _client.auth.currentSession?.accessToken;
|
|
final response = await _client.functions.invoke(
|
|
'admin_user_management',
|
|
body: payload,
|
|
headers: accessToken == null
|
|
? null
|
|
: {'Authorization': 'Bearer $accessToken'},
|
|
);
|
|
if (response.status != 200) {
|
|
throw Exception(response.data ?? 'Failed to reset password');
|
|
}
|
|
}
|
|
|
|
/// Set/unset a user's ban/lock via the admin Edge Function (preferred).
|
|
Future<void> setLock({required String userId, required bool locked}) async {
|
|
final payload = {'action': 'set_lock', 'userId': userId, 'locked': locked};
|
|
final accessToken = _client.auth.currentSession?.accessToken;
|
|
final response = await _client.functions.invoke(
|
|
'admin_user_management',
|
|
body: payload,
|
|
headers: accessToken == null
|
|
? null
|
|
: {'Authorization': 'Bearer $accessToken'},
|
|
);
|
|
if (response.status != 200) {
|
|
throw Exception(response.data ?? 'Failed to update lock state');
|
|
}
|
|
}
|
|
|
|
/// Fetch user email + banned state from the admin Edge Function (auth.user).
|
|
Future<AdminUserStatus> fetchStatus(String userId) async {
|
|
final payload = {'action': 'get_user', 'userId': userId};
|
|
final accessToken = _client.auth.currentSession?.accessToken;
|
|
final response = await _client.functions.invoke(
|
|
'admin_user_management',
|
|
body: payload,
|
|
headers: accessToken == null
|
|
? null
|
|
: {'Authorization': 'Bearer $accessToken'},
|
|
);
|
|
if (response.status != 200) {
|
|
return AdminUserStatus(email: null, bannedUntil: null);
|
|
}
|
|
final data = response.data;
|
|
final user = (data is Map<String, dynamic>)
|
|
? (data['user'] as Map<String, dynamic>?)
|
|
: null;
|
|
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),
|
|
);
|
|
}
|
|
|
|
/// Server-side paginated listing via Edge Function (returns auth + profile light view).
|
|
Future<List<Map<String, dynamic>>> listUsers(AdminUserQuery q) async {
|
|
final payload = {
|
|
'action': 'list_users',
|
|
'offset': q.offset,
|
|
'limit': q.limit,
|
|
'searchQuery': q.searchQuery,
|
|
};
|
|
final accessToken = _client.auth.currentSession?.accessToken;
|
|
final response = await _client.functions.invoke(
|
|
'admin_user_management',
|
|
body: payload,
|
|
headers: accessToken == null
|
|
? null
|
|
: {'Authorization': 'Bearer $accessToken'},
|
|
);
|
|
if (response.status != 200) {
|
|
throw Exception(response.data ?? 'Failed to list users');
|
|
}
|
|
final users = (response.data is Map && response.data['users'] is List)
|
|
? (response.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);
|
|
});
|