Skip to content

Commit 81382bc

Browse files
committed
feat: add api key creation logic
1 parent 0bfd7c4 commit 81382bc

File tree

13 files changed

+339
-708
lines changed

13 files changed

+339
-708
lines changed

api/src/__test__/core/__snapshots__/permissions.test.ts.snap

Lines changed: 0 additions & 457 deletions
This file was deleted.

api/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ try {
1212
const shellToUse = execSync('which bash').toString().trim();
1313
await CommandFactory.run(CliModule, {
1414
cliName: 'unraid-api',
15-
logger: false,
15+
logger: false, // new LogService(), - enable this to see nest initialization issues
1616
completion: {
1717
fig: true,
1818
cmd: 'unraid-api',

api/src/graphql/generated/api/operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ export function PciSchema(): z.ZodObject<Properties<Pci>> {
823823
export function PermissionSchema(): z.ZodObject<Properties<Permission>> {
824824
return z.object({
825825
__typename: z.literal('Permission').optional(),
826-
actions: z.array(z.string()).nullish(),
826+
actions: z.array(z.string()),
827827
resource: ResourceSchema
828828
})
829829
}

api/src/graphql/generated/api/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export type ApiKey = {
6666
description?: Maybe<Scalars['String']['output']>;
6767
id: Scalars['ID']['output'];
6868
name: Scalars['String']['output'];
69-
permissions?: Maybe<Array<Permission>>;
69+
permissions: Array<Permission>;
7070
roles: Array<Role>;
7171
};
7272

@@ -1028,7 +1028,7 @@ export type Pci = {
10281028

10291029
export type Permission = {
10301030
__typename?: 'Permission';
1031-
actions?: Maybe<Array<Scalars['String']['output']>>;
1031+
actions: Array<Scalars['String']['output']>;
10321032
resource: Resource;
10331033
};
10341034

@@ -2056,7 +2056,7 @@ export type ApiKeyResolvers<ContextType = Context, ParentType extends ResolversP
20562056
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
20572057
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
20582058
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
2059-
permissions?: Resolver<Maybe<Array<ResolversTypes['Permission']>>, ParentType, ContextType>;
2059+
permissions?: Resolver<Array<ResolversTypes['Permission']>, ParentType, ContextType>;
20602060
roles?: Resolver<Array<ResolversTypes['Role']>, ParentType, ContextType>;
20612061
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
20622062
}>;
@@ -2653,7 +2653,7 @@ export type PciResolvers<ContextType = Context, ParentType extends ResolversPare
26532653
}>;
26542654

26552655
export type PermissionResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Permission'] = ResolversParentTypes['Permission']> = ResolversObject<{
2656-
actions?: Resolver<Maybe<Array<ResolversTypes['String']>>, ParentType, ContextType>;
2656+
actions?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
26572657
resource?: Resolver<ResolversTypes['Resource'], ParentType, ContextType>;
26582658
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
26592659
}>;

api/src/graphql/schema/types/auth/auth.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
type Permission {
22
resource: Resource!
3-
actions: [String!]
3+
actions: [String!]!
44
}
55

66
type ApiKey {

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

Lines changed: 26 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { updateUserConfig } from '@app/store/modules/config';
1414
import { FileLoadStatus } from '@app/store/types';
1515

1616
import { ApiKeyService } from './api-key.service';
17+
import { environment } from '@app/environment';
1718

1819
// Mock the store and its modules
1920
vi.mock('@app/store', () => ({
@@ -84,6 +85,7 @@ describe('ApiKeyService', () => {
8485
};
8586

8687
beforeEach(async () => {
88+
environment.IS_MAIN_PROCESS = true;
8789
vi.resetAllMocks();
8890

8991
// Create mock logger methods
@@ -159,7 +161,7 @@ describe('ApiKeyService', () => {
159161
const { key, id, description, roles } = mockApiKeyWithSecret;
160162
const name = 'Test API Key';
161163

162-
const result = await apiKeyService.create(name, description ?? '', roles);
164+
const result = await apiKeyService.create({ name, description: description ?? '', roles });
163165

164166
expect(result).toMatchObject({
165167
id,
@@ -176,17 +178,23 @@ describe('ApiKeyService', () => {
176178
it('should validate input parameters', async () => {
177179
const saveSpy = vi.spyOn(apiKeyService, 'saveApiKey');
178180

179-
await expect(apiKeyService.create('', 'desc', [Role.GUEST])).rejects.toThrow(
181+
await expect(
182+
apiKeyService.create({ name: '', description: 'desc', roles: [Role.GUEST] })
183+
).rejects.toThrow(
180184
'API key name must contain only letters, numbers, and spaces (Unicode letters are supported)'
181185
);
182186

183-
await expect(apiKeyService.create('name', 'desc', [])).rejects.toThrow(
184-
'At least one role must be specified'
185-
);
187+
await expect(
188+
apiKeyService.create({ name: 'name', description: 'desc', roles: [] })
189+
).rejects.toThrow('At least one role must be specified');
186190

187-
await expect(apiKeyService.create('name', 'desc', ['invalid_role' as Role])).rejects.toThrow(
188-
'Invalid role specified'
189-
);
191+
await expect(
192+
apiKeyService.create({
193+
name: 'name',
194+
description: 'desc',
195+
roles: ['invalid_role' as Role],
196+
})
197+
).rejects.toThrow('Invalid role specified');
190198

191199
expect(saveSpy).not.toHaveBeenCalled();
192200
});
@@ -248,12 +256,12 @@ describe('ApiKeyService', () => {
248256

249257
await apiKeyService['createLocalApiKeyForConnectIfNecessary']();
250258

251-
expect(apiKeyService.create).toHaveBeenCalledWith(
252-
'Connect',
253-
'API key for Connect user',
254-
[Role.CONNECT],
255-
true
256-
);
259+
expect(apiKeyService.create).toHaveBeenCalledWith({
260+
name: 'Connect',
261+
description: 'API key for Connect user',
262+
roles: [Role.CONNECT],
263+
overwrite: true,
264+
});
257265
expect(store.dispatch).toHaveBeenCalledWith(
258266
updateUserConfig({
259267
remote: {
@@ -263,15 +271,12 @@ describe('ApiKeyService', () => {
263271
);
264272
});
265273

266-
it('should throw error if key creation fails', async () => {
274+
it('should log an error if key creation fails', async () => {
267275
vi.spyOn(apiKeyService, 'findByField').mockReturnValue(null);
268-
vi.spyOn(apiKeyService, 'create').mockResolvedValue({
269-
...mockApiKeyWithSecret,
270-
key: '', // Empty string instead of undefined/null
271-
} as ApiKeyWithSecret);
276+
vi.spyOn(apiKeyService, 'createLocalConnectApiKey').mockResolvedValue(null);
272277

273-
await expect(apiKeyService['createLocalApiKeyForConnectIfNecessary']()).rejects.toThrow(
274-
'Failed to create local API key'
278+
await expect(apiKeyService['createLocalApiKeyForConnectIfNecessary']()).resolves.toBe(
279+
undefined
275280
);
276281
expect(mockLogger.error).toHaveBeenCalledWith(
277282
'Failed to create local API key - no key returned'
@@ -431,60 +436,6 @@ describe('ApiKeyService', () => {
431436
});
432437
});
433438

434-
describe('findOneByKey', () => {
435-
it('should return UserAccount when API key exists', async () => {
436-
const findByKeySpy = vi
437-
.spyOn(apiKeyService, 'findByKey')
438-
.mockResolvedValue(mockApiKeyWithSecret);
439-
const result = await apiKeyService.findOneByKey('test-api-key');
440-
441-
expect(result).toEqual({
442-
id: mockApiKeyWithSecret.id,
443-
name: mockApiKeyWithSecret.name,
444-
description: mockApiKeyWithSecret.description,
445-
roles: mockApiKeyWithSecret.roles,
446-
});
447-
expect(findByKeySpy).toHaveBeenCalledWith('test-api-key');
448-
});
449-
450-
it('should use default description when none provided', async () => {
451-
const keyWithoutDesc = { ...mockApiKeyWithSecret, description: null };
452-
vi.spyOn(apiKeyService, 'findByKey').mockResolvedValue(keyWithoutDesc);
453-
const result = await apiKeyService.findOneByKey('test-api-key');
454-
455-
expect(result).toEqual({
456-
id: keyWithoutDesc.id,
457-
name: keyWithoutDesc.name,
458-
description: `API Key ${keyWithoutDesc.name}`,
459-
roles: keyWithoutDesc.roles,
460-
});
461-
});
462-
463-
it('should return null when API key not found', async () => {
464-
vi.spyOn(apiKeyService, 'findByKey').mockResolvedValue(null);
465-
466-
await expect(apiKeyService.findOneByKey('non-existent-key')).rejects.toThrow(
467-
'API key not found'
468-
);
469-
});
470-
471-
it('should throw error when API key not found', async () => {
472-
vi.spyOn(apiKeyService, 'findByKey').mockResolvedValue(null);
473-
474-
await expect(apiKeyService.findOneByKey('non-existent-key')).rejects.toThrow(
475-
'API key not found'
476-
);
477-
});
478-
479-
it('should throw error when unexpected error occurs', async () => {
480-
vi.spyOn(apiKeyService, 'findByKey').mockRejectedValue(new Error('Test error'));
481-
482-
await expect(apiKeyService.findOneByKey('test-api-key')).rejects.toThrow(
483-
'Failed to retrieve user account'
484-
);
485-
});
486-
});
487-
488439
describe('saveApiKey', () => {
489440
it('should save API key to file', async () => {
490441
vi.mocked(ApiKeyWithSecretSchema).mockReturnValue({

0 commit comments

Comments
 (0)