mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-11 02:49:36 -06:00
- Introduce AGENTS.md for repository guidelines and project structure - Add core development principles in .cursor/rules/core-development.mdc - Establish project-specific context in .cursor/rules/project-context.mdc - Implement Cursor IDE configuration in .cursor/rules/cursor.json - Create specialized rules for controllers, services, DTOs, guards, routes, and integrations - Update .gitignore to exclude unnecessary files - Enhance CLAUDE.md with project overview and common development commands
416 lines
10 KiB
Plaintext
416 lines
10 KiB
Plaintext
---
|
|
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<Auth>('AUTHENTICATION').API_KEY;
|
|
const key = req.get('apikey');
|
|
const db = configService.get<Database>('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<CacheConf>('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<void> {
|
|
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<Auth>('AUTHENTICATION');
|
|
const cacheConfig = configService.get<CacheConf>('CACHE');
|
|
const dbConfig = configService.get<Database>('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<CacheConf>('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<Request>;
|
|
let res: Partial<Response>;
|
|
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();
|
|
}
|
|
``` |