diff --git a/flutter_client/lib/admin/admin_providers.dart b/flutter_client/lib/admin/admin_providers.dart index 94ffefbe..dcc220e6 100644 --- a/flutter_client/lib/admin/admin_providers.dart +++ b/flutter_client/lib/admin/admin_providers.dart @@ -8,6 +8,7 @@ import '../library/library_providers.dart' show dioProvider; import '../models/admin_quarantine_item.dart'; import '../models/admin_request.dart'; import '../models/admin_user.dart'; +import '../models/invite.dart'; final adminRequestsApiProvider = FutureProvider((ref) async { return AdminRequestsApi(await ref.watch(dioProvider.future)); @@ -92,16 +93,73 @@ final adminRequestsProvider = AsyncNotifierProvider>( AdminRequestsController.new); -/// Read side defined here so the Requests screen can join requester -/// UUID → username for display. Mutation methods (setAdmin, -/// setAutoApprove, resetPassword, delete) are appended in Slice 3 -/// alongside the Users screen. +/// Read side originally defined for the Requests screen's +/// requester-UUID → username join. Mutation methods (setAdmin, +/// setAutoApprove, resetPassword, delete) added alongside the Users +/// screen in Slice 3. resetPassword is admin-supplies; the server +/// has no auto-generation mode. class AdminUsersController extends AsyncNotifier> { @override Future> build() async { final api = await ref.watch(adminUsersApiProvider.future); return api.list(); } + + /// Optimistic flip of `is_admin` on the user row; rolls back on + /// server error (including the last-admin guard 4xx). + Future setAdmin(String id, bool isAdmin) => + _patch(id, (u) => _copy(u, isAdmin: isAdmin), + (api) => api.setAdmin(id, isAdmin)); + + Future setAutoApprove(String id, bool autoApprove) => + _patch(id, (u) => _copy(u, autoApproveRequests: autoApprove), + (api) => api.setAutoApprove(id, autoApprove)); + + Future delete(String id) async { + final api = await ref.read(adminUsersApiProvider.future); + final current = state.value ?? const []; + state = AsyncData(current.where((u) => u.id != id).toList()); + try { + await api.delete(id); + ref.invalidate(adminCountsProvider); + } catch (e, st) { + state = AsyncData(current); + Error.throwWithStackTrace(e, st); + } + } + + Future resetPassword(String id, String newPassword) async { + final api = await ref.read(adminUsersApiProvider.future); + await api.resetPassword(id, newPassword); + } + + Future _patch( + String id, + AdminUser Function(AdminUser) mutate, + Future Function(AdminUsersApi api) action, + ) async { + final api = await ref.read(adminUsersApiProvider.future); + final current = state.value ?? const []; + state = AsyncData([ + for (final u in current) u.id == id ? mutate(u) : u, + ]); + try { + await action(api); + } catch (e, st) { + state = AsyncData(current); + Error.throwWithStackTrace(e, st); + } + } + + AdminUser _copy(AdminUser u, {bool? isAdmin, bool? autoApproveRequests}) => + AdminUser( + id: u.id, + username: u.username, + displayName: u.displayName, + isAdmin: isAdmin ?? u.isAdmin, + autoApproveRequests: autoApproveRequests ?? u.autoApproveRequests, + createdAt: u.createdAt, + ); } final adminUsersProvider = @@ -142,3 +200,38 @@ class AdminQuarantineController final adminQuarantineProvider = AsyncNotifierProvider>(AdminQuarantineController.new); + +class AdminInvitesController extends AsyncNotifier> { + @override + Future> build() async { + final api = await ref.watch(adminInvitesApiProvider.future); + return api.list(); + } + + /// Returns the freshly-minted invite so the screen can show the + /// token in a copy-once dialog. The new invite is also prepended + /// to the local list so it shows up without a refresh. + Future create({String? note}) async { + final api = await ref.read(adminInvitesApiProvider.future); + final invite = await api.create(note: note); + final current = state.value ?? const []; + state = AsyncData([invite, ...current]); + return invite; + } + + Future revoke(String token) async { + final api = await ref.read(adminInvitesApiProvider.future); + final current = state.value ?? const []; + state = AsyncData(current.where((i) => i.token != token).toList()); + try { + await api.revoke(token); + } catch (e, st) { + state = AsyncData(current); + Error.throwWithStackTrace(e, st); + } + } +} + +final adminInvitesProvider = + AsyncNotifierProvider>( + AdminInvitesController.new);