WIP: Added api-keys endpoints, openapi docs and tests
This commit is contained in:
parent
8dddaf5c76
commit
f732ddbe67
@ -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
|
||||
@ -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:
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -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() };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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' };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -151,6 +151,10 @@ export interface StorageKeyBuilder {
|
||||
*/
|
||||
buildUserKey(userId: string): string;
|
||||
|
||||
buildAccessRecordingSecretsKey(recordingId:string): string;
|
||||
/**
|
||||
* Builds Api Key
|
||||
*/
|
||||
buildApiKeysKey(): string;
|
||||
|
||||
buildAccessRecordingSecretsKey(recordingId: string): string;
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user