From 79d00ba00111f5439dc229e380c8b61007683b93 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 8 May 2026 21:52:00 -0400 Subject: [PATCH] feat(flutter/api): admin endpoint clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four small Dio-backed API classes mirroring the server's actual contract (verified against internal/api/admin_*.go): - Requests/Quarantine return flat lists; Users/Invites are enveloped ({"users": [...]}, {"invites": [...]}) and unwrap here - setAutoApprove sends {auto_approve: bool} — different from the response field name auto_approve_requests - resetPassword takes admin-supplied {password: string}, returns 204 - Invite create takes optional {note: string}; expiry is server-fixed at 24h Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/api/endpoints/admin_invites.dart | 32 +++++++++++++ .../lib/api/endpoints/admin_quarantine.dart | 31 ++++++++++++ .../lib/api/endpoints/admin_requests.dart | 26 ++++++++++ .../lib/api/endpoints/admin_users.dart | 47 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 flutter_client/lib/api/endpoints/admin_invites.dart create mode 100644 flutter_client/lib/api/endpoints/admin_quarantine.dart create mode 100644 flutter_client/lib/api/endpoints/admin_requests.dart create mode 100644 flutter_client/lib/api/endpoints/admin_users.dart diff --git a/flutter_client/lib/api/endpoints/admin_invites.dart b/flutter_client/lib/api/endpoints/admin_invites.dart new file mode 100644 index 00000000..e5ab6142 --- /dev/null +++ b/flutter_client/lib/api/endpoints/admin_invites.dart @@ -0,0 +1,32 @@ +import 'package:dio/dio.dart'; + +import '../../models/invite.dart'; + +class AdminInvitesApi { + AdminInvitesApi(this._dio); + final Dio _dio; + + /// GET /api/admin/invites → `{"invites": [...]}`. Envelope unwrapped. + Future> list() async { + final r = await _dio.get>('/api/admin/invites'); + final raw = (r.data?['invites'] as List?) ?? const []; + return raw + .map((e) => Invite.fromJson((e as Map).cast())) + .toList(growable: false); + } + + /// POST /api/admin/invites — body is optional `{"note": "..."}`. + /// Server hardcodes the 24h TTL; admins cannot configure expiry. + /// Returns the bare invite (not enveloped). + Future create({String? note}) async { + final r = await _dio.post>( + '/api/admin/invites', + data: {if (note != null && note.isNotEmpty) 'note': note}, + ); + return Invite.fromJson(r.data ?? const {}); + } + + Future revoke(String token) async { + await _dio.delete('/api/admin/invites/$token'); + } +} diff --git a/flutter_client/lib/api/endpoints/admin_quarantine.dart b/flutter_client/lib/api/endpoints/admin_quarantine.dart new file mode 100644 index 00000000..e72736c0 --- /dev/null +++ b/flutter_client/lib/api/endpoints/admin_quarantine.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; + +import '../../models/admin_quarantine_item.dart'; + +class AdminQuarantineApi { + AdminQuarantineApi(this._dio); + final Dio _dio; + + /// GET /api/admin/quarantine — flat list (no envelope) of aggregated + /// quarantine rows. + Future> list() async { + final r = await _dio.get>('/api/admin/quarantine'); + final raw = r.data ?? const []; + return raw + .map((e) => + AdminQuarantineItem.fromJson((e as Map).cast())) + .toList(growable: false); + } + + Future resolve(String trackId) async { + await _dio.post('/api/admin/quarantine/$trackId/resolve'); + } + + Future deleteFile(String trackId) async { + await _dio.post('/api/admin/quarantine/$trackId/delete-file'); + } + + Future deleteViaLidarr(String trackId) async { + await _dio.post('/api/admin/quarantine/$trackId/delete-via-lidarr'); + } +} diff --git a/flutter_client/lib/api/endpoints/admin_requests.dart b/flutter_client/lib/api/endpoints/admin_requests.dart new file mode 100644 index 00000000..519cd7bd --- /dev/null +++ b/flutter_client/lib/api/endpoints/admin_requests.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; + +import '../../models/admin_request.dart'; + +class AdminRequestsApi { + AdminRequestsApi(this._dio); + final Dio _dio; + + /// GET /api/admin/requests — flat list of pending requests. + /// Server defaults to ?status=pending; we don't override. + Future> list() async { + final r = await _dio.get>('/api/admin/requests'); + final raw = r.data ?? const []; + return raw + .map((e) => AdminRequest.fromJson((e as Map).cast())) + .toList(growable: false); + } + + Future approve(String id) async { + await _dio.post('/api/admin/requests/$id/approve'); + } + + Future reject(String id) async { + await _dio.post('/api/admin/requests/$id/reject'); + } +} diff --git a/flutter_client/lib/api/endpoints/admin_users.dart b/flutter_client/lib/api/endpoints/admin_users.dart new file mode 100644 index 00000000..92252c57 --- /dev/null +++ b/flutter_client/lib/api/endpoints/admin_users.dart @@ -0,0 +1,47 @@ +import 'package:dio/dio.dart'; + +import '../../models/admin_user.dart'; + +class AdminUsersApi { + AdminUsersApi(this._dio); + final Dio _dio; + + /// GET /api/admin/users → `{"users": [...]}`. Envelope unwrapped here. + Future> list() async { + final r = await _dio.get>('/api/admin/users'); + final raw = (r.data?['users'] as List?) ?? const []; + return raw + .map((e) => AdminUser.fromJson((e as Map).cast())) + .toList(growable: false); + } + + Future setAdmin(String id, bool isAdmin) async { + await _dio.put( + '/api/admin/users/$id/admin', + data: {'is_admin': isAdmin}, + ); + } + + /// Server's body field is `auto_approve` (NOT `auto_approve_requests`) + /// — different from the response field name. Verified against + /// internal/api/admin_users.go `adminAutoApproveReq` (May 2026). + Future setAutoApprove(String id, bool autoApprove) async { + await _dio.put( + '/api/admin/users/$id/auto-approve', + data: {'auto_approve': autoApprove}, + ); + } + + /// Admin supplies the new password; server returns 204. There is no + /// server-generated-password mode. + Future resetPassword(String id, String newPassword) async { + await _dio.post( + '/api/admin/users/$id/reset-password', + data: {'password': newPassword}, + ); + } + + Future delete(String id) async { + await _dio.delete('/api/admin/users/$id'); + } +}