feat(flutter/api): admin endpoint clients

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 21:52:00 -04:00
parent 2b595c40cd
commit 79d00ba001
4 changed files with 136 additions and 0 deletions
@@ -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<Invite>> list() async {
final r = await _dio.get<Map<String, dynamic>>('/api/admin/invites');
final raw = (r.data?['invites'] as List?) ?? const [];
return raw
.map((e) => Invite.fromJson((e as Map).cast<String, dynamic>()))
.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<Invite> create({String? note}) async {
final r = await _dio.post<Map<String, dynamic>>(
'/api/admin/invites',
data: {if (note != null && note.isNotEmpty) 'note': note},
);
return Invite.fromJson(r.data ?? const {});
}
Future<void> revoke(String token) async {
await _dio.delete<void>('/api/admin/invites/$token');
}
}
@@ -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<AdminQuarantineItem>> list() async {
final r = await _dio.get<List<dynamic>>('/api/admin/quarantine');
final raw = r.data ?? const [];
return raw
.map((e) =>
AdminQuarantineItem.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
Future<void> resolve(String trackId) async {
await _dio.post<void>('/api/admin/quarantine/$trackId/resolve');
}
Future<void> deleteFile(String trackId) async {
await _dio.post<void>('/api/admin/quarantine/$trackId/delete-file');
}
Future<void> deleteViaLidarr(String trackId) async {
await _dio.post<void>('/api/admin/quarantine/$trackId/delete-via-lidarr');
}
}
@@ -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<AdminRequest>> list() async {
final r = await _dio.get<List<dynamic>>('/api/admin/requests');
final raw = r.data ?? const [];
return raw
.map((e) => AdminRequest.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
Future<void> approve(String id) async {
await _dio.post<void>('/api/admin/requests/$id/approve');
}
Future<void> reject(String id) async {
await _dio.post<void>('/api/admin/requests/$id/reject');
}
}
@@ -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<AdminUser>> list() async {
final r = await _dio.get<Map<String, dynamic>>('/api/admin/users');
final raw = (r.data?['users'] as List?) ?? const [];
return raw
.map((e) => AdminUser.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false);
}
Future<void> setAdmin(String id, bool isAdmin) async {
await _dio.put<void>(
'/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<void> setAutoApprove(String id, bool autoApprove) async {
await _dio.put<void>(
'/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<void> resetPassword(String id, String newPassword) async {
await _dio.post<void>(
'/api/admin/users/$id/reset-password',
data: {'password': newPassword},
);
}
Future<void> delete(String id) async {
await _dio.delete<void>('/api/admin/users/$id');
}
}