AI Implementation feature(867): Admin Area Players Management (#56)

This commit was merged in pull request #56.
This commit is contained in:
2026-07-23 08:08:50 +00:00
parent 705530e71f
commit e4ccf0fe13
27 changed files with 2039 additions and 218 deletions
@@ -10,6 +10,7 @@ const FRIENDLY_MESSAGES: Record<string, string> = {
FORBIDDEN: 'You are not allowed to perform this action.',
NOT_FOUND: 'Not found.',
VALIDATION_FAILED: 'Invalid input.',
LAST_ADMIN: 'At least one admin must remain in the system. The action was rejected.',
};
function friendlyMessage(status: number, body: any): string {
@@ -48,5 +49,7 @@ function shouldSuppress(url: string | undefined, status: number): boolean {
if (status === 401 && /\/api\/v1\/auth\/(login|csrf|register)/.test(url)) return true;
// SSE / event-status snapshot errors are non-fatal (SSE will deliver the next frame).
if (status === 401 && url.endsWith('/api/v1/challenges/status')) return true;
// Admin Players modal owns its own inline error presentation.
if (/\/api\/v1\/admin\/players\/.+\/password/.test(url)) return true;
return false;
}
@@ -6,6 +6,19 @@ export interface AdminUser {
id: string;
username: string;
role: 'admin' | 'player';
status?: 'enabled' | 'disabled';
createdAt?: string;
}
export interface AdminResetPasswordBody {
newPassword: string;
confirmPassword: string;
}
export class AdminLastAdminError extends Error {
constructor() {
super('At least one admin must remain in the system.');
}
}
export interface GeneralSettings {
@@ -172,8 +185,57 @@ export class AdminService {
private readonly http = inject(HttpClient);
async listUsers(): Promise<AdminUser[]> {
return this.listPlayers();
}
async listPlayers(): Promise<AdminUser[]> {
const rows = await firstValueFrom(
this.http.get<AdminUser[]>('/api/v1/admin/players', { withCredentials: true }),
);
return rows.map((u) => ({
id: u.id,
username: u.username,
role: u.role,
status: u.status ?? 'enabled',
createdAt: u.createdAt ?? new Date().toISOString(),
}));
}
async updatePlayerRole(id: string, role: 'admin' | 'player'): Promise<AdminUser> {
return firstValueFrom(
this.http.get<AdminUser[]>('/api/v1/admin/users', { withCredentials: true }),
this.http.patch<AdminUser>(
`/api/v1/admin/players/${encodeURIComponent(id)}/role`,
{ role },
{ withCredentials: true },
),
);
}
async updatePlayerStatus(id: string, enabled: boolean): Promise<AdminUser> {
return firstValueFrom(
this.http.patch<AdminUser>(
`/api/v1/admin/players/${encodeURIComponent(id)}/status`,
{ enabled },
{ withCredentials: true },
),
);
}
async resetPlayerPassword(id: string, body: AdminResetPasswordBody): Promise<void> {
await firstValueFrom(
this.http.patch<void>(
`/api/v1/admin/players/${encodeURIComponent(id)}/password`,
body,
{ withCredentials: true },
),
);
}
async deletePlayer(id: string): Promise<void> {
await firstValueFrom(
this.http.delete<void>(`/api/v1/admin/players/${encodeURIComponent(id)}`, {
withCredentials: true,
}),
);
}