refactor: api key response dto (#30887)
This commit is contained in:
1 parent
a066b5f301
commit
47c5a3dbf6
8 files changed
+104
-37
No files matched your search
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in new issue
Block a user