diff --git a/api/src/cli/commands/key.ts b/api/src/cli/commands/key.ts new file mode 100644 index 000000000..69e8610e1 --- /dev/null +++ b/api/src/cli/commands/key.ts @@ -0,0 +1,117 @@ +import { ArgumentConfig, parse } from 'ts-command-line-args'; + +import { cliLogger } from '@app/core/log'; +import { Role } from '@app/graphql/generated/api/types'; +import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; + +enum Command { + Get = 'get', + Create = 'create', +} + +type KeyFlags = { + command: string; + name: string; + create?: boolean; + roles?: string; + permissions?: string; +}; + +const validRoles: Set = new Set(Object.values(Role)); + +const validateRoles = (rolesStr?: string): Role[] => { + if (!rolesStr) return [Role.GUEST]; + + const requestedRoles = rolesStr.split(',').map((role) => role.trim().toUpperCase() as Role); + const validRequestedRoles = requestedRoles.filter((role) => validRoles.has(role)); + + if (validRequestedRoles.length === 0) { + throw new Error(`Invalid roles. Valid options are: ${Array.from(validRoles).join(', ')}`); + } + + const invalidRoles = requestedRoles.filter((role) => !validRoles.has(role)); + + if (invalidRoles.length > 0) { + cliLogger.warn(`Ignoring invalid roles: ${invalidRoles.join(', ')}`); + } + + return validRequestedRoles; +}; + +const keyOptions: ArgumentConfig = { + command: { type: String, description: 'get or create' }, + name: { type: String, description: 'Name of the API key', typeLabel: '{underline name}' }, + create: { type: Boolean, optional: true, description: "Create the key if it doesn't exist" }, + roles: { + type: String, + optional: true, + description: `Comma-separated list of roles (${Object.values(Role).join(', ')})`, + typeLabel: '{underline role1,role2}', + }, + permissions: { + type: String, + optional: true, + description: 'Comma-separated list of permissions', + typeLabel: '{underline perm1,perm2}', + }, +}; + +export const key = async (...argv: string[]) => { + try { + const options = parse(keyOptions, { argv }); + const apiKeyService = new ApiKeyService(); + + if (!options.name) { + throw new Error('Name is required'); + } + + switch (options.command) { + case Command.Create: { + const roles = validateRoles(options.roles); + const key = await apiKeyService.create( + options.name, + `CLI generated key: ${options.name}`, + roles, + true + ); + + console.log('API Key: ', key); + cliLogger.info('API key created successfully'); + + break; + } + + case Command.Get: { + const key = await apiKeyService.findByField('name', options.name); + + if (!key && options.create) { + const roles = validateRoles(options.roles); + const newKey = await apiKeyService.create( + options.name, + `CLI generated key: ${options.name}`, + roles, + true + ); + + console.log('New API Key: ', newKey); + cliLogger.info('API key created successfully'); + } else if (key) { + console.log('API Key: ', key); + } else { + throw new Error(`No API key found with name: ${options.name}`); + } + + break; + } + + default: + throw new Error(`Invalid command. Use: ${Object.values(Command).join(' or ')}`); + } + } catch (error) { + if (error instanceof Error) { + cliLogger.error(`Failed to process API key: ${error.message}`); + } + + process.exit(1); + } +}; diff --git a/api/src/cli/index.ts b/api/src/cli/index.ts index 7695d630a..e1d4e46c5 100755 --- a/api/src/cli/index.ts +++ b/api/src/cli/index.ts @@ -30,6 +30,7 @@ export const main = async (...argv: string[]) => { // Only import the command we need when we use it const commands = { + key: import('@app/cli/commands/key').then((pkg) => pkg.key), start: import('@app/cli/commands/start').then((pkg) => pkg.start), stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop), restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart),