feat: get metadata about the current api key (#21027)

This commit is contained in:
Jason Rasmussen
2025-08-18 19:15:03 -04:00
committed by GitHub
parent a313e4338e
commit e00556a34a
12 changed files with 174 additions and 12 deletions
+36 -1
View File
@@ -1,4 +1,4 @@
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { Permission } from 'src/enum';
import { ApiKeyService } from 'src/services/api-key.service';
import { factory, newUuid } from 'test/small.factory';
@@ -134,6 +134,41 @@ describe(ApiKeyService.name, () => {
});
});
describe('getMine', () => {
it('should not work with a session token', async () => {
const session = factory.session();
const auth = factory.auth({ session });
mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.getMine(auth)).rejects.toBeInstanceOf(ForbiddenException);
expect(mocks.apiKey.getById).not.toHaveBeenCalled();
});
it('should throw an error if the key is not found', async () => {
const apiKey = factory.authApiKey();
const auth = factory.auth({ apiKey });
mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.getMine(auth)).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.apiKey.getById).toHaveBeenCalledWith(auth.user.id, apiKey.id);
});
it('should get a key by id', async () => {
const auth = factory.auth();
const apiKey = factory.apiKey({ userId: auth.user.id });
mocks.apiKey.getById.mockResolvedValue(apiKey);
await sut.getById(auth, apiKey.id);
expect(mocks.apiKey.getById).toHaveBeenCalledWith(auth.user.id, apiKey.id);
});
});
describe('getById', () => {
it('should throw an error if the key is not found', async () => {
const auth = factory.auth();
+14 -1
View File
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { ApiKey } from 'src/database';
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -46,6 +46,19 @@ export class ApiKeyService extends BaseService {
await this.apiKeyRepository.delete(auth.user.id, id);
}
async getMine(auth: AuthDto): Promise<APIKeyResponseDto> {
if (!auth.apiKey) {
throw new ForbiddenException('Not authenticated with an API Key');
}
const key = await this.apiKeyRepository.getById(auth.user.id, auth.apiKey.id);
if (!key) {
throw new BadRequestException('API Key not found');
}
return this.map(key);
}
async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
const key = await this.apiKeyRepository.getById(auth.user.id, id);
if (!key) {
+14
View File
@@ -518,6 +518,20 @@ describe(AuthService.name, () => {
await expect(result).rejects.toThrow('Missing required permission: all');
});
it('should not require any permission when metadata is set to `false`', async () => {
const authUser = factory.authUser();
const authApiKey = factory.authApiKey({ permissions: [Permission.ActivityRead] });
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser });
const result = sut.authenticate({
headers: { 'x-api-key': 'auth_token' },
queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: false },
});
await expect(result).resolves.toEqual({ user: authUser, apiKey: expect.objectContaining(authApiKey) });
});
it('should return an auth dto', async () => {
const authUser = factory.authUser();
const authApiKey = factory.authApiKey({ permissions: [Permission.All] });
+7 -2
View File
@@ -48,7 +48,8 @@ export type ValidateRequest = {
metadata: {
sharedLinkRoute: boolean;
adminRoute: boolean;
permission?: Permission;
/** `false` explicitly means no permission is required, which otherwise defaults to `all` */
permission?: Permission | false;
uri: string;
};
};
@@ -187,7 +188,11 @@ export class AuthService extends BaseService {
throw new ForbiddenException('Forbidden');
}
if (authDto.apiKey && !isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions })) {
if (
authDto.apiKey &&
requestedPermission !== false &&
!isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions })
) {
throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);
}