mirror of
https://github.com/unraid/api.git
synced 2025-12-31 05:29:48 -06:00
feat(api): add unraid-api --delete command (#1289)
- Added a `command:raw` package.json script to let devs run `dist/cli.js` without rebuilding, enabling experimentation in the transpiled script. - `unraid-api --delete` allows users to select and remove keys with confirmation prompts.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"start": "node dist/main.js",
|
||||
"dev": "vite",
|
||||
"command": "pnpm run build && clear && ./dist/cli.js",
|
||||
"command:raw": "./dist/cli.js",
|
||||
"// Build and Deploy": "",
|
||||
"build": "vite build --mode=production",
|
||||
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import crypto from 'crypto';
|
||||
import { readdir, readFile, writeFile } from 'fs/promises';
|
||||
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { watch } from 'chokidar';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { setLocalApiKey } from '@app/store/modules/config.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
import { batchProcess } from '@app/utils.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService implements OnModuleInit {
|
||||
@@ -312,4 +313,36 @@ export class ApiKeyService implements OnModuleInit {
|
||||
basePath: this.basePath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes API keys from the disk and updates the in-memory store.
|
||||
*
|
||||
* This method first verifies that all the provided API key IDs exist in the in-memory store.
|
||||
* If any keys are missing, it throws an Error detailing the missing keys.
|
||||
* It then deletes the corresponding JSON files concurrently using batch processing.
|
||||
* If any errors occur during the file deletion process, an array of errors is thrown.
|
||||
*
|
||||
* @param ids An array of API key identifiers to delete.
|
||||
* @throws Error if one or more API keys are not found.
|
||||
* @throws Array<Error> if errors occur during the file deletion.
|
||||
*/
|
||||
public async deleteApiKeys(ids: string[]): Promise<void> {
|
||||
// First verify all keys exist
|
||||
const missingKeys = ids.filter((id) => !this.findByField('id', id));
|
||||
if (missingKeys.length > 0) {
|
||||
throw new Error(`API keys not found: ${missingKeys.join(', ')}`);
|
||||
}
|
||||
|
||||
// Delete all files in parallel
|
||||
const { errors, data: deletedIds } = await batchProcess(ids, async (id) => {
|
||||
await unlink(join(this.basePath, `${id}.json`));
|
||||
return id;
|
||||
});
|
||||
|
||||
const deletedSet = new Set(deletedIds);
|
||||
this.memoryApiKeys = this.memoryApiKeys.filter((key) => !deletedSet.has(key.id));
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ import { AuthActionVerb } from 'nest-authz';
|
||||
import { Command, CommandRunner, InquirerService, Option } from 'nest-commander';
|
||||
|
||||
import type { Permission } from '@app/graphql/generated/api/types.js';
|
||||
import type { DeleteApiKeyAnswers } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { Resource, Role } from '@app/graphql/generated/api/types.js';
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
|
||||
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
|
||||
interface KeyOptions {
|
||||
name: string;
|
||||
create: boolean;
|
||||
delete?: boolean;
|
||||
description?: string;
|
||||
roles?: Role[];
|
||||
permissions?: Permission[];
|
||||
@@ -17,7 +20,7 @@ interface KeyOptions {
|
||||
|
||||
@Command({
|
||||
name: 'apikey',
|
||||
description: `Create / Fetch Connect API Keys - use --create with no arguments for a creation wizard`,
|
||||
description: `Create / Fetch / Delete Connect API Keys - use --create with no arguments for a creation wizard, or --delete to remove keys`,
|
||||
})
|
||||
export class ApiKeyCommand extends CommandRunner {
|
||||
constructor(
|
||||
@@ -88,8 +91,50 @@ ACTIONS: ${Object.values(AuthActionVerb).join(', ')}`,
|
||||
return description;
|
||||
}
|
||||
|
||||
async run(_: string[], options: KeyOptions = { create: false, name: '' }): Promise<void> {
|
||||
@Option({
|
||||
flags: '--delete',
|
||||
description: 'Delete selected API keys',
|
||||
})
|
||||
parseDelete(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Prompt the user to select API keys to delete. Then, delete the selected keys. */
|
||||
private async deleteKeys() {
|
||||
const allKeys = this.apiKeyService.findAll();
|
||||
if (allKeys.length === 0) {
|
||||
this.logger.log('No API keys found to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
const answers = await this.inquirerService.prompt<DeleteApiKeyAnswers>(
|
||||
DeleteApiKeyQuestionSet.name,
|
||||
{}
|
||||
);
|
||||
if (!answers.selectedKeys || answers.selectedKeys.length === 0) {
|
||||
this.logger.log('No keys selected for deletion');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiKeyService.deleteApiKeys(answers.selectedKeys);
|
||||
this.logger.log(`Successfully deleted ${answers.selectedKeys.length} API keys`);
|
||||
} catch (error) {
|
||||
this.logger.error(error as any);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async run(
|
||||
_: string[],
|
||||
options: KeyOptions = { create: false, name: '', delete: false }
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (options.delete) {
|
||||
await this.deleteKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this.apiKeyService.findByField('name', options.name);
|
||||
if (key) {
|
||||
this.logger.log(key.key);
|
||||
|
||||
35
api/src/unraid-api/cli/apikey/delete-api-key.questions.ts
Normal file
35
api/src/unraid-api/cli/apikey/delete-api-key.questions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ChoicesFor, Question, QuestionSet } from 'nest-commander';
|
||||
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
|
||||
export interface DeleteApiKeyAnswers {
|
||||
selectedKeys: string[];
|
||||
}
|
||||
|
||||
@QuestionSet({ name: 'delete-api-key' })
|
||||
export class DeleteApiKeyQuestionSet {
|
||||
constructor(
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly logger: LogService
|
||||
) {}
|
||||
|
||||
static name = 'delete-api-key';
|
||||
|
||||
@Question({
|
||||
name: 'selectedKeys',
|
||||
type: 'checkbox',
|
||||
message: 'Select API keys to delete:',
|
||||
})
|
||||
parseSelectedKeys(keyIds: string[]): string[] {
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
@ChoicesFor({ name: 'selectedKeys' })
|
||||
async getKeys() {
|
||||
return this.apiKeyService.findAll().map((key) => ({
|
||||
name: `${key.name} (${key.description ?? ''}) [${key.id}]`,
|
||||
value: key.id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CommandRunner } from 'nest-commander';
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
|
||||
import { ApiKeyCommand } from '@app/unraid-api/cli/apikey/api-key.command.js';
|
||||
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { ConfigCommand } from '@app/unraid-api/cli/config.command.js';
|
||||
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
|
||||
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
|
||||
@@ -50,6 +51,7 @@ const DEFAULT_COMMANDS = [
|
||||
|
||||
const DEFAULT_PROVIDERS = [
|
||||
AddApiKeyQuestionSet,
|
||||
DeleteApiKeyQuestionSet,
|
||||
AddSSOUserQuestionSet,
|
||||
RemoveSSOUserQuestionSet,
|
||||
DeveloperQuestions,
|
||||
|
||||
Reference in New Issue
Block a user