Skip to content

Commit 4cff1a8

Browse files
committed
feat(api): add unraid-api --delete command
1 parent 234017a commit 4cff1a8

File tree

5 files changed

+119
-3
lines changed

5 files changed

+119
-3
lines changed

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"start": "node dist/main.js",
1818
"dev": "vite",
1919
"command": "pnpm run build && clear && ./dist/cli.js",
20+
"command:raw": "./dist/cli.js",
2021
"// Build and Deploy": "",
2122
"build": "vite build --mode=production",
2223
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",

api/src/unraid-api/auth/api-key.service.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
22
import crypto from 'crypto';
3-
import { readdir, readFile, writeFile } from 'fs/promises';
3+
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
44
import { join } from 'path';
55

66
import { watch } from 'chokidar';
@@ -23,6 +23,7 @@ import {
2323
import { getters, store } from '@app/store/index.js';
2424
import { setLocalApiKey } from '@app/store/modules/config.js';
2525
import { FileLoadStatus } from '@app/store/types.js';
26+
import { batchProcess } from '@app/utils.js';
2627

2728
@Injectable()
2829
export class ApiKeyService implements OnModuleInit {
@@ -312,4 +313,36 @@ export class ApiKeyService implements OnModuleInit {
312313
basePath: this.basePath,
313314
};
314315
}
316+
317+
/**
318+
* Deletes API keys from the disk and updates the in-memory store.
319+
*
320+
* This method first verifies that all the provided API key IDs exist in the in-memory store.
321+
* If any keys are missing, it throws an Error detailing the missing keys.
322+
* It then deletes the corresponding JSON files concurrently using batch processing.
323+
* If any errors occur during the file deletion process, an array of errors is thrown.
324+
*
325+
* @param ids An array of API key identifiers to delete.
326+
* @throws Error if one or more API keys are not found.
327+
* @throws Array<Error> if errors occur during the file deletion.
328+
*/
329+
public async deleteApiKeys(ids: string[]): Promise<void> {
330+
// First verify all keys exist
331+
const missingKeys = ids.filter((id) => !this.findByField('id', id));
332+
if (missingKeys.length > 0) {
333+
throw new Error(`API keys not found: ${missingKeys.join(', ')}`);
334+
}
335+
336+
// Delete all files in parallel
337+
const { errors, data: deletedIds } = await batchProcess(ids, async (id) => {
338+
await unlink(join(this.basePath, `${id}.json`));
339+
return id;
340+
});
341+
342+
const deletedSet = new Set(deletedIds);
343+
this.memoryApiKeys = this.memoryApiKeys.filter((key) => !deletedSet.has(key.id));
344+
if (errors.length > 0) {
345+
throw errors;
346+
}
347+
}
315348
}

api/src/unraid-api/cli/apikey/api-key.command.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@ import { AuthActionVerb } from 'nest-authz';
22
import { Command, CommandRunner, InquirerService, Option } from 'nest-commander';
33

44
import type { Permission } from '@app/graphql/generated/api/types.js';
5+
import type { DeleteApiKeyAnswers } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
56
import { Resource, Role } from '@app/graphql/generated/api/types.js';
67
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
78
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
9+
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
810
import { LogService } from '@app/unraid-api/cli/log.service.js';
911

1012
interface KeyOptions {
1113
name: string;
1214
create: boolean;
15+
delete?: boolean;
1316
description?: string;
1417
roles?: Role[];
1518
permissions?: Permission[];
1619
}
1720

1821
@Command({
1922
name: 'apikey',
20-
description: `Create / Fetch Connect API Keys - use --create with no arguments for a creation wizard`,
23+
description: `Create / Fetch / Delete Connect API Keys - use --create with no arguments for a creation wizard, or --delete to remove keys`,
2124
})
2225
export class ApiKeyCommand extends CommandRunner {
2326
constructor(
@@ -88,8 +91,50 @@ ACTIONS: ${Object.values(AuthActionVerb).join(', ')}`,
8891
return description;
8992
}
9093

91-
async run(_: string[], options: KeyOptions = { create: false, name: '' }): Promise<void> {
94+
@Option({
95+
flags: '--delete',
96+
description: 'Delete selected API keys',
97+
})
98+
parseDelete(): boolean {
99+
return true;
100+
}
101+
102+
/** Prompt the user to select API keys to delete. Then, delete the selected keys. */
103+
private async deleteKeys() {
104+
const allKeys = this.apiKeyService.findAll();
105+
if (allKeys.length === 0) {
106+
this.logger.log('No API keys found to delete');
107+
return;
108+
}
109+
110+
const answers = await this.inquirerService.prompt<DeleteApiKeyAnswers>(
111+
DeleteApiKeyQuestionSet.name,
112+
{}
113+
);
114+
if (!answers.selectedKeys || answers.selectedKeys.length === 0) {
115+
this.logger.log('No keys selected for deletion');
116+
return;
117+
}
118+
119+
try {
120+
await this.apiKeyService.deleteApiKeys(answers.selectedKeys);
121+
this.logger.log(`Successfully deleted ${answers.selectedKeys.length} API keys`);
122+
} catch (error) {
123+
this.logger.error(error as any);
124+
process.exit(1);
125+
}
126+
}
127+
128+
async run(
129+
_: string[],
130+
options: KeyOptions = { create: false, name: '', delete: false }
131+
): Promise<void> {
92132
try {
133+
if (options.delete) {
134+
await this.deleteKeys();
135+
return;
136+
}
137+
93138
const key = this.apiKeyService.findByField('name', options.name);
94139
if (key) {
95140
this.logger.log(key.key);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ChoicesFor, Question, QuestionSet } from 'nest-commander';
2+
3+
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
4+
import { LogService } from '@app/unraid-api/cli/log.service.js';
5+
6+
export interface DeleteApiKeyAnswers {
7+
selectedKeys: string[];
8+
}
9+
10+
@QuestionSet({ name: 'delete-api-key' })
11+
export class DeleteApiKeyQuestionSet {
12+
constructor(
13+
private readonly apiKeyService: ApiKeyService,
14+
private readonly logger: LogService
15+
) {}
16+
17+
static name = 'delete-api-key';
18+
19+
@Question({
20+
name: 'selectedKeys',
21+
type: 'checkbox',
22+
message: 'Select API keys to delete:',
23+
})
24+
parseSelectedKeys(val: string[]): string[] {
25+
return val;
26+
}
27+
28+
@ChoicesFor({ name: 'selectedKeys' })
29+
async getKeys() {
30+
return this.apiKeyService.findAll().map((key) => ({
31+
name: `${key.name} (${key.description ?? ''}) [${key.id}]`,
32+
value: key.id,
33+
}));
34+
}
35+
}

api/src/unraid-api/cli/cli.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CommandRunner } from 'nest-commander';
55
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
66
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
77
import { ApiKeyCommand } from '@app/unraid-api/cli/apikey/api-key.command.js';
8+
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
89
import { ConfigCommand } from '@app/unraid-api/cli/config.command.js';
910
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
1011
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
@@ -50,6 +51,7 @@ const DEFAULT_COMMANDS = [
5051

5152
const DEFAULT_PROVIDERS = [
5253
AddApiKeyQuestionSet,
54+
DeleteApiKeyQuestionSet,
5355
AddSSOUserQuestionSet,
5456
RemoveSSOUserQuestionSet,
5557
DeveloperQuestions,

0 commit comments

Comments
 (0)