--- description: Guard patterns for authentication and authorization in Evolution API globs: - "src/api/guards/**/*.ts" alwaysApply: false --- # Evolution API Guard Rules ## Guard Structure Pattern ### Standard Guard Function ```typescript import { NextFunction, Request, Response } from 'express'; import { Logger } from '@config/logger.config'; import { UnauthorizedException, ForbiddenException } from '@exceptions'; const logger = new Logger('GUARD'); async function guardFunction(req: Request, _: Response, next: NextFunction) { // Guard logic here if (validationFails) { throw new UnauthorizedException(); } return next(); } export const guardName = { guardFunction }; ``` ## Authentication Guard Pattern ### API Key Authentication ```typescript async function apikey(req: Request, _: Response, next: NextFunction) { const env = configService.get('AUTHENTICATION').API_KEY; const key = req.get('apikey'); const db = configService.get('DATABASE'); if (!key) { throw new UnauthorizedException(); } // Global API key check if (env.KEY === key) { return next(); } // Special routes handling if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { throw new ForbiddenException('Missing global api key', 'The global api key must be set'); } const param = req.params as unknown as InstanceDto; try { if (param?.instanceName) { const instance = await prismaRepository.instance.findUnique({ where: { name: param.instanceName }, }); if (instance.token === key) { return next(); } } else { if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) { const instanceByKey = await prismaRepository.instance.findFirst({ where: { token: key }, }); if (instanceByKey) { return next(); } } } } catch (error) { logger.error(error); } throw new UnauthorizedException(); } export const authGuard = { apikey }; ``` ## Instance Validation Guards ### Instance Exists Guard ```typescript async function getInstance(instanceName: string) { try { const cacheConf = configService.get('CACHE'); const exists = !!waMonitor.waInstances[instanceName]; if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) { const keyExists = await cache.has(instanceName); return exists || keyExists; } return exists || (await prismaRepository.instance.findMany({ where: { name: instanceName } })).length > 0; } catch (error) { throw new InternalServerErrorException(error?.toString()); } } export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) { if (req.originalUrl.includes('/instance/create')) { return next(); } const param = req.params as unknown as InstanceDto; if (!param?.instanceName) { throw new BadRequestException('"instanceName" not provided.'); } if (!(await getInstance(param.instanceName))) { throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); } next(); } ``` ### Instance Logged Guard ```typescript export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) { if (req.originalUrl.includes('/instance/create')) { const instance = req.body as InstanceDto; if (await getInstance(instance.instanceName)) { throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); } if (waMonitor.waInstances[instance.instanceName]) { delete waMonitor.waInstances[instance.instanceName]; } } next(); } ``` ## Telemetry Guard Pattern ### Telemetry Collection ```typescript class Telemetry { public collectTelemetry(req: Request, res: Response, next: NextFunction): void { // Collect telemetry data const telemetryData = { route: req.originalUrl, method: req.method, timestamp: new Date(), userAgent: req.get('User-Agent'), }; // Send telemetry asynchronously (don't block request) setImmediate(() => { this.sendTelemetry(telemetryData); }); next(); } private async sendTelemetry(data: any): Promise { try { // Send telemetry data } catch (error) { // Silently fail - don't affect main request } } } export default Telemetry; ``` ## Guard Composition Pattern ### Multiple Guards Usage ```typescript // In router setup const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard['apikey']]; router .use('/instance', new InstanceRouter(configService, ...guards).router) .use('/message', new MessageRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router); ``` ## Error Handling in Guards ### Proper Exception Throwing ```typescript // CORRECT - Use proper HTTP exceptions if (!apiKey) { throw new UnauthorizedException('API key required'); } if (instanceExists) { throw new ForbiddenException('Instance already exists'); } if (!instanceFound) { throw new NotFoundException('Instance not found'); } if (validationFails) { throw new BadRequestException('Invalid request parameters'); } // INCORRECT - Don't use generic Error if (!apiKey) { throw new Error('API key required'); // ❌ Use specific exceptions } ``` ## Configuration Access in Guards ### Config Service Usage ```typescript async function configAwareGuard(req: Request, _: Response, next: NextFunction) { const authConfig = configService.get('AUTHENTICATION'); const cacheConfig = configService.get('CACHE'); const dbConfig = configService.get('DATABASE'); // Use configuration for guard logic if (authConfig.API_KEY.KEY === providedKey) { return next(); } throw new UnauthorizedException(); } ``` ## Database Access in Guards ### Prisma Repository Usage ```typescript async function databaseGuard(req: Request, _: Response, next: NextFunction) { try { const param = req.params as unknown as InstanceDto; const instance = await prismaRepository.instance.findUnique({ where: { name: param.instanceName }, }); if (!instance) { throw new NotFoundException('Instance not found'); } // Additional validation logic if (instance.status !== 'active') { throw new ForbiddenException('Instance not active'); } return next(); } catch (error) { logger.error('Database guard error:', error); throw new InternalServerErrorException('Database access failed'); } } ``` ## Cache Integration in Guards ### Cache Service Usage ```typescript async function cacheAwareGuard(req: Request, _: Response, next: NextFunction) { const cacheConf = configService.get('CACHE'); if (cacheConf.REDIS.ENABLED) { const cached = await cache.get(`guard:${req.params.instanceName}`); if (cached) { // Use cached validation result return next(); } } // Perform validation and cache result const isValid = await performValidation(req.params.instanceName); if (cacheConf.REDIS.ENABLED) { await cache.set(`guard:${req.params.instanceName}`, isValid, 300); // 5 min TTL } if (isValid) { return next(); } throw new UnauthorizedException(); } ``` ## Logging in Guards ### Structured Logging ```typescript const logger = new Logger('GUARD'); async function loggedGuard(req: Request, _: Response, next: NextFunction) { logger.log(`Guard validation started for ${req.originalUrl}`); try { // Guard logic const isValid = await validateRequest(req); if (isValid) { logger.log(`Guard validation successful for ${req.params.instanceName}`); return next(); } logger.warn(`Guard validation failed for ${req.params.instanceName}`); throw new UnauthorizedException(); } catch (error) { logger.error(`Guard validation error: ${error.message}`, error.stack); throw error; } } ``` ## Guard Testing Pattern ### Unit Test Structure ```typescript describe('authGuard', () => { let req: Partial; let res: Partial; let next: NextFunction; beforeEach(() => { req = { get: jest.fn(), params: {}, originalUrl: '/test', }; res = {}; next = jest.fn(); }); describe('apikey', () => { it('should pass with valid global API key', async () => { (req.get as jest.Mock).mockReturnValue('valid-global-key'); await authGuard.apikey(req as Request, res as Response, next); expect(next).toHaveBeenCalled(); }); it('should throw UnauthorizedException with no API key', async () => { (req.get as jest.Mock).mockReturnValue(undefined); await expect( authGuard.apikey(req as Request, res as Response, next) ).rejects.toThrow(UnauthorizedException); }); it('should pass with valid instance token', async () => { (req.get as jest.Mock).mockReturnValue('instance-token'); req.params = { instanceName: 'test-instance' }; // Mock prisma repository jest.spyOn(prismaRepository.instance, 'findUnique').mockResolvedValue({ token: 'instance-token', } as any); await authGuard.apikey(req as Request, res as Response, next); expect(next).toHaveBeenCalled(); }); }); }); ``` ## Guard Performance Considerations ### Efficient Validation ```typescript // CORRECT - Efficient guard with early returns async function efficientGuard(req: Request, _: Response, next: NextFunction) { // Quick checks first if (req.originalUrl.includes('/public')) { return next(); // Skip validation for public routes } const apiKey = req.get('apikey'); if (!apiKey) { throw new UnauthorizedException(); // Fail fast } // More expensive checks only if needed if (apiKey === globalKey) { return next(); // Skip database check } // Database check only as last resort const isValid = await validateInDatabase(apiKey); if (isValid) { return next(); } throw new UnauthorizedException(); } // INCORRECT - Inefficient guard async function inefficientGuard(req: Request, _: Response, next: NextFunction) { // Always do expensive database check first const dbResult = await expensiveDatabaseQuery(); // ❌ Expensive operation first const apiKey = req.get('apikey'); if (!apiKey && dbResult) { // ❌ Complex logic throw new UnauthorizedException(); } next(); } ```