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'
|
$ref: './paths/internal/auth.yaml#/~1auth~1logout'
|
||||||
/auth/refresh:
|
/auth/refresh:
|
||||||
$ref: './paths/internal/auth.yaml#/~1auth~1refresh'
|
$ref: './paths/internal/auth.yaml#/~1auth~1refresh'
|
||||||
|
/auth/api-keys:
|
||||||
|
$ref: './paths/internal/auth.yaml#/~1auth~1api-keys'
|
||||||
/users/profile:
|
/users/profile:
|
||||||
$ref: './paths/internal/users.yaml#/~1users~1profile'
|
$ref: './paths/internal/users.yaml#/~1users~1profile'
|
||||||
/users/change-password:
|
/users/change-password:
|
||||||
|
|||||||
@ -63,3 +63,56 @@
|
|||||||
$ref: '../../components/responses/internal/error-invalid-refresh-token.yaml'
|
$ref: '../../components/responses/internal/error-invalid-refresh-token.yaml'
|
||||||
'500':
|
'500':
|
||||||
$ref: '../../components/responses/internal-server-error.yaml'
|
$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_ROOMS_PREFIX: 'rooms',
|
||||||
S3_RECORDINGS_PREFIX: 'recordings',
|
S3_RECORDINGS_PREFIX: 'recordings',
|
||||||
S3_USERS_PREFIX: 'users',
|
S3_USERS_PREFIX: 'users',
|
||||||
|
S3_API_KEYS_PREFIX: 'api_keys',
|
||||||
|
|
||||||
// Garbage collection and recording lock intervals
|
// Garbage collection and recording lock intervals
|
||||||
ROOM_GC_INTERVAL: '1h' as StringValue, // e.g. garbage collector interval for rooms
|
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');
|
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 bcrypt from 'bcrypt';
|
||||||
|
import { uid } from 'uid/secure';
|
||||||
|
|
||||||
const SALT_ROUNDS = 10;
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
@ -10,4 +11,9 @@ export class PasswordHelper {
|
|||||||
static async verifyPassword(password: string, hash: string): Promise<boolean> {
|
static async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
return bcrypt.compare(password, hash);
|
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 = `${RedisKeyPrefix.BASE}recording:`,
|
||||||
RECORDING_SECRETS = `${RedisKeyPrefix.BASE}recording_secrets:`,
|
RECORDING_SECRETS = `${RedisKeyPrefix.BASE}recording_secrets:`,
|
||||||
ARCHIVED_ROOM = `${RedisKeyPrefix.BASE}archived_room:`,
|
ARCHIVED_ROOM = `${RedisKeyPrefix.BASE}archived_room:`,
|
||||||
USER = `${RedisKeyPrefix.BASE}user:`
|
USER = `${RedisKeyPrefix.BASE}user:`,
|
||||||
|
API_KEYS = `${RedisKeyPrefix.BASE}api_keys:`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum RedisLockPrefix {
|
export const enum RedisLockPrefix {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as authCtrl from '../controllers/auth.controller.js';
|
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();
|
export const authRouter = Router();
|
||||||
authRouter.use(bodyParser.urlencoded({ extended: true }));
|
authRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||||
@ -11,3 +12,8 @@ authRouter.use(bodyParser.json());
|
|||||||
authRouter.post('/login', validateLoginRequest, withLoginLimiter, authCtrl.login);
|
authRouter.post('/login', validateLoginRequest, withLoginLimiter, authCtrl.login);
|
||||||
authRouter.post('/logout', authCtrl.logout);
|
authRouter.post('/logout', authCtrl.logout);
|
||||||
authRouter.post('/refresh', authCtrl.refreshToken);
|
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 { User } from '@typings-ce';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from 'inversify';
|
||||||
import { PasswordHelper } from '../helpers/index.js';
|
import { PasswordHelper } from '../helpers/index.js';
|
||||||
import { UserService } from './index.js';
|
import { MeetStorageService, UserService } from './index.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class AuthService {
|
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> {
|
async authenticate(username: string, password: string): Promise<User | null> {
|
||||||
const user = await this.userService.getUser(username);
|
const user = await this.userService.getUser(username);
|
||||||
@ -16,4 +19,20 @@ export class AuthService {
|
|||||||
|
|
||||||
return user;
|
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`;
|
return `${INTERNAL_CONFIG.S3_USERS_PREFIX}/${userId}.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildApiKeysKey(): string {
|
||||||
|
return `${INTERNAL_CONFIG.S3_API_KEYS_PREFIX}.json`;
|
||||||
|
}
|
||||||
|
|
||||||
buildAccessRecordingSecretsKey(recordingId: string): string {
|
buildAccessRecordingSecretsKey(recordingId: string): string {
|
||||||
const { roomId, egressId, uid } = RecordingHelper.extractInfoFromRecordingId(recordingId);
|
const { roomId, egressId, uid } = RecordingHelper.extractInfoFromRecordingId(recordingId);
|
||||||
return `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.secrets/${roomId}/${egressId}/${uid}.json`;
|
return `${INTERNAL_CONFIG.S3_RECORDINGS_PREFIX}/.secrets/${roomId}/${egressId}/${uid}.json`;
|
||||||
|
|||||||
@ -151,6 +151,10 @@ export interface StorageKeyBuilder {
|
|||||||
*/
|
*/
|
||||||
buildUserKey(userId: string): string;
|
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);
|
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
|
// PRIVATE HELPER METHODS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -796,10 +829,6 @@ export class MeetStorageService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// PRIVATE HELPER METHODS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default global preferences.
|
* Returns the default global preferences.
|
||||||
* @returns {GPrefs}
|
* @returns {GPrefs}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user