mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-20 12:22:21 -06:00
feat: add project guidelines and configuration files for development standards
- 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
This commit is contained in:
416
.cursor/rules/specialized-rules/guard-rules.mdc
Normal file
416
.cursor/rules/specialized-rules/guard-rules.mdc
Normal file
@@ -0,0 +1,416 @@
|
||||
---
|
||||
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();
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user