WIP: Added api-keys endpoints, openapi docs and tests

This commit is contained in:
Carlos Santos 2025-06-18 09:19:28 +02:00
parent 8dddaf5c76
commit f732ddbe67
12 changed files with 195 additions and 9 deletions

View File

@ -0,0 +1,19 @@
description: Create a new API key.
required: true
content:
application/json:
schema:
type: object
properties:
apiKey:
type: string
description: The API key to be created.
example: "ovmeet-1234567890abcdef1234567890abcdef"
creationDate:
type: string
format: date-time
description: The date and time when the API key was created.
example: "2023-10-01T12:00:00Z"
required:
- apiKey
additionalProperties: false

View File

@ -14,6 +14,8 @@ paths:
$ref: './paths/internal/auth.yaml#/~1auth~1logout'
/auth/refresh:
$ref: './paths/internal/auth.yaml#/~1auth~1refresh'
/auth/api-keys:
$ref: './paths/internal/auth.yaml#/~1auth~1api-keys'
/users/profile:
$ref: './paths/internal/users.yaml#/~1users~1profile'
/users/change-password:

View File

@ -63,3 +63,56 @@
$ref: '../../components/responses/internal/error-invalid-refresh-token.yaml'
'500':
$ref: '../../components/responses/internal-server-error.yaml'
/auth/api-keys:
post:
operationId: createApiKey
summary: Create an API key
description: >
Creates a new API key. Only one API key can be created in the system.
If an API key already exists, it will be replaced with the new one.
The API key is returned in the response.
tags:
- Internal API - Authentication
security:
- accessTokenCookie: []
requestBody:
$ref: '../../components/requestBodies/internal/create-api-key.yaml'
responses:
'201':
$ref: '../../components/responses/internal/success-create-api-key.yaml'
'400':
$ref: '../../components/responses/internal/error-invalid-request.yaml'
'500':
$ref: '../../components/responses/internal-server-error.yaml'
get:
operationId: getApiKeys
summary: Get the API keys
description: >
Retrieves the existing API keys.
tags:
- Internal API - Authentication
security:
- accessTokenCookie: []
responses:
'200':
$ref: '../../components/responses/internal/success-get-api-key.yaml'
'500':
$ref: '../../components/responses/internal-server-error.yaml'
delete:
operationId: deleteApiKeys
summary: Delete the API keys
description: >
Deletes the existing API keys.
tags:
- Internal API - Authentication
security:
- accessTokenCookie: []
responses:
'204':
description: Successfully deleted the API key
'500':
$ref: '../../components/responses/internal-server-error.yaml'

View File

@ -25,6 +25,7 @@ const INTERNAL_CONFIG = {
S3_ROOMS_PREFIX: 'rooms',
S3_RECORDINGS_PREFIX: 'recordings',
S3_USERS_PREFIX: 'users',
S3_API_KEYS_PREFIX: 'api_keys',
// Garbage collection and recording lock intervals
ROOM_GC_INTERVAL: '1h' as StringValue, // e.g. garbage collector interval for rooms

View File

@ -102,3 +102,45 @@ export const refreshToken = async (req: Request, res: Response) => {
handleError(res, error, 'refreshing token');
}
};
export const createApiKey = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
logger.verbose('Create API key request received');
const authService = container.get(AuthService);
try {
const apiKey = await authService.createApiKey();
return res.status(201).json({ apiKey });
} catch (error) {
handleError(res, error, 'creating API key');
}
};
export const deleteApiKeys = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
logger.verbose('Delete API keys request received');
const authService = container.get(AuthService);
try {
await authService.deleteApiKeys();
return res.status(202).send();
} catch (error) {
handleError(res, error, 'deleting API keys');
}
};
export const getApiKeys = async (_req: Request, res: Response) => {
const logger = container.get(LoggerService);
logger.verbose('Get API keys request received');
const authService = container.get(AuthService);
try {
const apiKeys = await authService.getApiKeys();
return res.status(200).json({ apiKeys });
} catch (error) {
handleError(res, error, 'getting API keys');
}
};

View File

@ -1,4 +1,5 @@
import bcrypt from 'bcrypt';
import { uid } from 'uid/secure';
const SALT_ROUNDS = 10;
@ -10,4 +11,9 @@ export class PasswordHelper {
static async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Generate a secure API key using uid with a length of 32 characters
static generateApiKey(): { key: string; creationDate: string } {
return { key: `ovmeet-${uid(32)}`, creationDate: new Date().toISOString() };
}
}

View File

@ -8,7 +8,8 @@ export const enum RedisKeyName {
RECORDING = `${RedisKeyPrefix.BASE}recording:`,
RECORDING_SECRETS = `${RedisKeyPrefix.BASE}recording_secrets:`,
ARCHIVED_ROOM = `${RedisKeyPrefix.BASE}archived_room:`,
USER = `${RedisKeyPrefix.BASE}user:`
USER = `${RedisKeyPrefix.BASE}user:`,
API_KEYS = `${RedisKeyPrefix.BASE}api_keys:`,
}
export const enum RedisLockPrefix {

View File

@ -1,7 +1,8 @@
import bodyParser from 'body-parser';
import { Router } from 'express';
import * as authCtrl from '../controllers/auth.controller.js';
import { validateLoginRequest, withLoginLimiter } from '../middlewares/index.js';
import { tokenAndRoleValidator, validateLoginRequest, withAuth, withLoginLimiter } from '../middlewares/index.js';
import { UserRole } from '@typings-ce';
export const authRouter = Router();
authRouter.use(bodyParser.urlencoded({ extended: true }));
@ -11,3 +12,8 @@ authRouter.use(bodyParser.json());
authRouter.post('/login', validateLoginRequest, withLoginLimiter, authCtrl.login);
authRouter.post('/logout', authCtrl.logout);
authRouter.post('/refresh', authCtrl.refreshToken);
// API Key Routes
authRouter.post('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.createApiKey);
authRouter.get('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.getApiKeys);
authRouter.delete('/api-keys', withAuth(tokenAndRoleValidator(UserRole.ADMIN)), authCtrl.deleteApiKeys);

View File

@ -1,11 +1,14 @@
import { User } from '@typings-ce';
import { inject, injectable } from 'inversify';
import { PasswordHelper } from '../helpers/index.js';
import { UserService } from './index.js';
import { MeetStorageService, UserService } from './index.js';
@injectable()
export class AuthService {
constructor(@inject(UserService) protected userService: UserService) {}
constructor(
@inject(UserService) protected userService: UserService,
@inject(MeetStorageService) protected storageService: MeetStorageService
) {}
async authenticate(username: string, password: string): Promise<User | null> {
const user = await this.userService.getUser(username);
@ -16,4 +19,20 @@ export class AuthService {
return user;
}
async createApiKey() {
const apiKey = PasswordHelper.generateApiKey();
await this.storageService.saveApiKey(apiKey);
return apiKey;
}
async getApiKeys() {
const apiKeys = await this.storageService.getApiKeys();
return apiKeys;
}
async deleteApiKeys() {
await this.storageService.deleteApiKeys();
return { message: 'API keys deleted successfully' };
}
}

View File

@ -39,6 +39,10 @@ export class S3KeyBuilder implements StorageKeyBuilder {
return `${INTERNAL_CONFIG.S3_USERS_PREFIX}/${userId}.json`;
}
buildApiKeysKey(): string {
return `${INTERNAL_CONFIG.S3_API_KEYS_PREFIX}.json`;
}
buildAccessRecordingSecretsKey(recordingId: string): string {
const { roomId, egressId, uid } = RecordingHelper.extractInfoFromRecordingId(recordingId);
return `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.secrets/${roomId}/${egressId}/${uid}.json`;

View File

@ -151,6 +151,10 @@ export interface StorageKeyBuilder {
*/
buildUserKey(userId: string): string;
buildAccessRecordingSecretsKey(recordingId:string): string;
/**
* Builds Api Key
*/
buildApiKeysKey(): string;
buildAccessRecordingSecretsKey(recordingId: string): string;
}

View File

@ -568,6 +568,39 @@ export class MeetStorageService<
return await this.saveCacheAndStorage(userRedisKey, storageUserKey, user);
}
async saveApiKey(apiKeyData: { key: string; creationDate: string }): Promise<void> {
const redisKey = RedisKeyName.API_KEYS;
const storageKey = this.keyBuilder.buildApiKeysKey();
this.logger.debug(`Saving API key to Redis and storage: ${redisKey}`);
await this.saveCacheAndStorage(redisKey, storageKey, apiKeyData);
}
async getApiKeys(): Promise<{ key: string; creationDate: string }[]> {
const redisKey = RedisKeyName.API_KEYS;
const storageKey = this.keyBuilder.buildApiKeysKey();
this.logger.debug(`Retrieving API key from Redis and storage: ${redisKey}`);
const apiKeys = await this.getFromCacheAndStorage<{ key: string; creationDate: string }[]>(
redisKey,
storageKey
);
if (!apiKeys || apiKeys.length === 0) {
this.logger.warn('API key not found in cache or storage');
return [];
}
this.logger.debug(`Retrieved API key from storage: ${storageKey}`);
return apiKeys;
}
async deleteApiKeys(): Promise<void> {
const redisKey = RedisKeyName.API_KEYS;
const storageKey = this.keyBuilder.buildApiKeysKey();
this.logger.debug(`Deleting API key from Redis and storage: ${redisKey}`);
await this.deleteFromCacheAndStorage(redisKey, storageKey);
this.logger.verbose(`API key deleted successfully from storage: ${storageKey}`);
}
// ==========================================
// PRIVATE HELPER METHODS
// ==========================================
@ -796,10 +829,6 @@ export class MeetStorageService<
}
}
// ==========================================
// PRIVATE HELPER METHODS
// ==========================================
/**
* Returns the default global preferences.
* @returns {GPrefs}