refactor: api key response dto (#30887)

This commit is contained in:
Jason Rasmussen authored and GitHub committed 2026-08-20 11:23:12 -04:00
1 parent a066b5f301
commit 47c5a3dbf6
8 files changed
+104 -37

No files matched your search

+29 -22
View File
@@ -34,14 +34,14 @@ describe('/api-keys', () => {
permissions: [Permission.ApiKeyRead],
});
expect(body).toEqual({
id: expect.any(String),
name: 'API Key',
permissions: [Permission.ApiKeyRead],
createdAt: expect.any(String),
updatedAt: expect.any(String),
secret: expect.any(String),
apiKey: {
id: expect.any(String),
name: 'API Key',
permissions: [Permission.ApiKeyRead],
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
// TODO: remove in v4
apiKey: expect.any(Object),
});
expect(status).toBe(201);
});
@@ -72,14 +72,14 @@ describe('/api-keys', () => {
.send({ name: 'API Key', permissions: [Permission.All] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual({
apiKey: {
id: expect.any(String),
name: 'API Key',
permissions: [Permission.All],
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
id: expect.any(String),
name: 'API Key',
permissions: [Permission.All],
createdAt: expect.any(String),
updatedAt: expect.any(String),
secret: expect.any(String),
// TODO: remove in v4
apiKey: expect.any(Object),
});
expect(status).toEqual(201);
});
@@ -93,23 +93,30 @@ describe('/api-keys', () => {
});
it('should return a list of api keys', async () => {
const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([
const [apiKey1, apiKey2, apiKey3] = await Promise.all([
create(admin.accessToken, [Permission.All]),
create(admin.accessToken, [Permission.All]),
create(admin.accessToken, [Permission.All]),
]);
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(3);
expect(body).toEqual(expect.arrayContaining([apiKey1, apiKey2, apiKey3]));
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: apiKey1.id }),
expect.objectContaining({ id: apiKey2.id }),
expect.objectContaining({ id: apiKey3.id }),
]),
);
expect(status).toEqual(200);
});
});
describe('GET /api-keys/:id', () => {
it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { id } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`)
.get(`/api-keys/${id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
@@ -124,9 +131,9 @@ describe('/api-keys', () => {
describe('PUT /api-keys/:id', () => {
it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { id } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`)
.put(`/api-keys/${id}`)
.send({
name: 'new name',
permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate],
@@ -145,9 +152,9 @@ describe('/api-keys', () => {
describe('DELETE /api-keys/:id', () => {
it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { id } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app)
.delete(`/api-keys/${apiKey.id}`)
.delete(`/api-keys/${id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(204);
});
+4 -4
View File
@@ -30,8 +30,8 @@ describe(`immich login`, () => {
it('should login and save auth.yml with 600', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]);
const apiKey = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, apiKey.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285/api',
'Logged in as admin@immich.cloud',
@@ -47,8 +47,8 @@ describe(`immich login`, () => {
it('should login without /api in the url', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]);
const apiKey = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), apiKey.secret]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2285',
'Discovered API at http://127.0.0.1:2285/api',
+3 -3
View File
@@ -675,9 +675,9 @@ export const utils = {
},
cliLogin: async (accessToken: string) => {
const key = await utils.createApiKey(accessToken, [Permission.All]);
await immichCli(['login', app, key.secret]);
return key.secret;
const { secret } = await utils.createApiKey(accessToken, [Permission.All]);
await immichCli(['login', app, secret]);
return secret;
},
scan: async (accessToken: string, id: string) => {
+49 -2
View File
@@ -16979,16 +16979,63 @@
"ApiKeyCreateResponseDto": {
"properties": {
"apiKey": {
"$ref": "#/components/schemas/ApiKeyResponseDto"
"$ref": "#/components/schemas/ApiKeyResponseDto",
"x-immich-history": [
{
"version": "v1",
"state": "Added"
},
{
"version": "v3.2.0",
"state": "Deprecated"
}
],
"x-immich-state": "Deprecated"
},
"createdAt": {
"description": "Creation date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"type": "string"
},
"id": {
"description": "API key ID",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
},
"name": {
"description": "API key name",
"type": "string"
},
"permissions": {
"description": "List of permissions",
"items": {
"$ref": "#/components/schemas/Permission"
},
"type": "array"
},
"secret": {
"description": "API key secret (only shown once)",
"type": "string"
},
"updatedAt": {
"description": "Last update date",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"type": "string"
}
},
"required": [
"apiKey",
"secret"
"createdAt",
"id",
"name",
"permissions",
"secret",
"updatedAt"
],
"type": "object"
},
+10
View File
@@ -631,8 +631,18 @@ export type ApiKeyCreateDto = {
};
export type ApiKeyCreateResponseDto = {
apiKey: ApiKeyResponseDto;
/** Creation date */
createdAt: string;
/** API key ID */
id: string;
/** API key name */
name: string;
/** List of permissions */
permissions: Permission[];
/** API key secret (only shown once) */
secret: string;
/** Last update date */
updatedAt: string;
};
export type ApiKeyUpdateDto = {
/** API key name */
+3 -1
View File
@@ -1,4 +1,5 @@
import { createZodDto } from 'nestjs-zod';
import { HistoryBuilder } from 'src/decorators';
import { Permission } from 'src/enum';
import { isoDatetimeToDate } from 'src/validation';
import z from 'zod';
@@ -31,8 +32,9 @@ const ApiKeyResponseSchema = z
const ApiKeyCreateResponseSchema = z
.object({
...ApiKeyResponseSchema.shape,
secret: z.string().describe('API key secret (only shown once)'),
apiKey: ApiKeyResponseSchema,
apiKey: ApiKeyResponseSchema.meta({ ...new HistoryBuilder().added('v1').deprecated('v3.2.0').getExtensions() }),
})
.meta({ id: 'ApiKeyCreateResponseDto' });
+4 -3
View File
@@ -23,8 +23,9 @@ export class ApiKeyService extends BaseService {
userId: auth.user.id,
permissions: dto.permissions,
});
const apiKey = this.map(entity);
return { secret: token, apiKey: this.map(entity) };
return { ...apiKey, secret: token, apiKey };
}
async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise<ApiKeyResponseDto> {
@@ -58,10 +59,10 @@ export class ApiKeyService extends BaseService {
const token = this.cryptoRepository.randomBytesAsText(32);
const hashed = this.cryptoRepository.hashSha256(token);
const newKey = await this.apiKeyRepository.update(auth.user.id, id, { key: hashed });
const apiKey = this.map(newKey);
return { secret: token, apiKey: this.map(newKey) };
return { ...apiKey, secret: token, apiKey };
}
async delete(auth: AuthDto, id: string): Promise<void> {
+2 -2
View File
@@ -64,7 +64,7 @@ export const handleCreateApiKey = async (dto: ApiKeyCreateDto) => {
}
const response = await createApiKey({ apiKeyCreateDto: dto });
eventManager.emit('ApiKeyCreate', response.apiKey);
eventManager.emit('ApiKeyCreate', response);
return response;
} catch (error) {
@@ -105,7 +105,7 @@ export const handleRotateApiKey = async (apiKey: ApiKeyResponseDto) => {
try {
const response = await rotateApiKey({ id: apiKey.id });
eventManager.emit('ApiKeyUpdate', response.apiKey);
eventManager.emit('ApiKeyUpdate', response);
await modalManager.show(ApiKeySecretModal, { secret: response.secret });
} catch (error) {
handleError(error, $t('errors.something_went_wrong'));