AI Implementation feature(861): Admin Area Challenges: List, Import/Export and Add/Edit Modal (#40)
This commit was merged in pull request #40.
This commit is contained in:
@@ -63,6 +63,110 @@ export interface LogoUploadResponse {
|
||||
originalFilename: string;
|
||||
}
|
||||
|
||||
export interface AdminChallengeFile {
|
||||
id: string;
|
||||
originalFilename: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AdminChallengeListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
categoryAbbreviation: string;
|
||||
categoryIconPath: string;
|
||||
difficulty: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
protocol: 'NC' | 'WEB';
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
fileCount: number;
|
||||
solveCount: number;
|
||||
}
|
||||
|
||||
export interface AdminChallengeDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
descriptionMd: string;
|
||||
categoryId: string;
|
||||
categoryAbbreviation: string;
|
||||
categoryIconPath: string;
|
||||
difficulty: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
initialPoints: number;
|
||||
minimumPoints: number;
|
||||
decaySolves: number;
|
||||
flag: string;
|
||||
protocol: 'NC' | 'WEB';
|
||||
port: number | null;
|
||||
ipAddress: string;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
files: AdminChallengeFile[];
|
||||
}
|
||||
|
||||
export interface ChallengeFileManifestPayload {
|
||||
id?: string;
|
||||
stagedToken?: string;
|
||||
originalFilename: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
remove?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateChallengeBody {
|
||||
name: string;
|
||||
descriptionMd: string;
|
||||
categoryId: string;
|
||||
difficulty: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
initialPoints: number;
|
||||
minimumPoints: number;
|
||||
decaySolves: number;
|
||||
flag: string;
|
||||
protocol: 'NC' | 'WEB';
|
||||
port: number | null;
|
||||
ipAddress: string;
|
||||
enabled: boolean;
|
||||
files?: ChallengeFileManifestPayload[];
|
||||
}
|
||||
|
||||
export interface UpdateChallengeBody {
|
||||
name?: string;
|
||||
descriptionMd?: string;
|
||||
categoryId?: string;
|
||||
difficulty?: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
initialPoints?: number;
|
||||
minimumPoints?: number;
|
||||
decaySolves?: number;
|
||||
flag?: string;
|
||||
protocol?: 'NC' | 'WEB';
|
||||
port?: number | null;
|
||||
ipAddress?: string;
|
||||
enabled?: boolean;
|
||||
files?: ChallengeFileManifestPayload[];
|
||||
}
|
||||
|
||||
export interface ImportPreviewResponse {
|
||||
total: number;
|
||||
conflicts: string[];
|
||||
unknownCategories: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface ImportResultResponse {
|
||||
imported: number;
|
||||
skipped: { name: string; reason: string }[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface StageChallengeFileResponse {
|
||||
token: string;
|
||||
originalFilename: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminService {
|
||||
private readonly http = inject(HttpClient);
|
||||
@@ -139,4 +243,99 @@ export class AdminService {
|
||||
this.http.post<LogoUploadResponse>('/api/v1/uploads/logo', fd, { withCredentials: true }),
|
||||
);
|
||||
}
|
||||
|
||||
async listChallenges(query: { q?: string; categoryId?: string } = {}): Promise<AdminChallengeListItem[]> {
|
||||
const params: Record<string, string> = {};
|
||||
if (query.q && query.q.trim().length > 0) params['q'] = query.q.trim();
|
||||
if (query.categoryId) params['categoryId'] = query.categoryId;
|
||||
return firstValueFrom(
|
||||
this.http.get<AdminChallengeListItem[]>('/api/v1/admin/challenges', {
|
||||
params,
|
||||
withCredentials: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getChallengeDetail(id: string): Promise<AdminChallengeDetail> {
|
||||
return firstValueFrom(
|
||||
this.http.get<AdminChallengeDetail>(
|
||||
`/api/v1/admin/challenges/${encodeURIComponent(id)}`,
|
||||
{ withCredentials: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async createChallenge(body: CreateChallengeBody): Promise<AdminChallengeDetail> {
|
||||
return firstValueFrom(
|
||||
this.http.post<AdminChallengeDetail>('/api/v1/admin/challenges', body, {
|
||||
withCredentials: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async updateChallenge(id: string, body: UpdateChallengeBody): Promise<AdminChallengeDetail> {
|
||||
return firstValueFrom(
|
||||
this.http.put<AdminChallengeDetail>(
|
||||
`/api/v1/admin/challenges/${encodeURIComponent(id)}`,
|
||||
body,
|
||||
{ withCredentials: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteChallenge(id: string): Promise<void> {
|
||||
await firstValueFrom(
|
||||
this.http.delete<void>(`/api/v1/admin/challenges/${encodeURIComponent(id)}`, {
|
||||
withCredentials: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async exportChallenges(): Promise<Blob> {
|
||||
return firstValueFrom(
|
||||
this.http.get('/api/v1/admin/challenges/export', {
|
||||
withCredentials: true,
|
||||
responseType: 'blob',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async previewChallengeImport(doc: unknown): Promise<ImportPreviewResponse> {
|
||||
return firstValueFrom(
|
||||
this.http.post<ImportPreviewResponse>('/api/v1/admin/challenges/import', doc, {
|
||||
withCredentials: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async confirmChallengeImport(doc: unknown): Promise<ImportResultResponse> {
|
||||
return firstValueFrom(
|
||||
this.http.post<ImportResultResponse>(
|
||||
'/api/v1/admin/challenges/import?confirm=overwrite',
|
||||
doc,
|
||||
{ withCredentials: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async stageChallengeFile(file: File): Promise<StageChallengeFileResponse> {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return firstValueFrom(
|
||||
this.http.post<StageChallengeFileResponse>(
|
||||
'/api/v1/admin/challenges/files/stage',
|
||||
fd,
|
||||
{ withCredentials: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async discardChallengeFile(token: string): Promise<void> {
|
||||
await firstValueFrom(
|
||||
this.http.delete<void>(
|
||||
`/api/v1/admin/challenges/files/stage/${encodeURIComponent(token)}`,
|
||||
{ withCredentials: true },
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user