Skip to content

Commit

Permalink
feat(middleware): add rate-limit
Browse files Browse the repository at this point in the history
  • Loading branch information
filipedeschamps committed Aug 10, 2022
1 parent 4ab4b6a commit 6f82ceb
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 130 deletions.
6 changes: 4 additions & 2 deletions errors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ export class ValidationError extends BaseError {
}

export class UnauthorizedError extends BaseError {
constructor({ message, action, stack, errorLocationCode }) {
constructor({ message, action, requestId, stack, errorLocationCode }) {
super({
message: message || 'Usuário não autenticado.',
action: action || 'Verifique se você está autenticado com uma sessão ativa e tente novamente.',
requestId: requestId,
statusCode: 401,
stack: stack,
errorLocationCode: errorLocationCode,
Expand All @@ -101,10 +102,11 @@ export class UnauthorizedError extends BaseError {
}

export class ForbiddenError extends BaseError {
constructor({ message, action, stack, errorLocationCode }) {
constructor({ message, action, requestId, stack, errorLocationCode }) {
super({
message: message || 'Você não possui permissão para executar esta ação.',
action: action || 'Verifique se você possui permissão para executar esta ação.',
requestId: requestId,
statusCode: 403,
stack: stack,
errorLocationCode: errorLocationCode,
Expand Down
91 changes: 91 additions & 0 deletions infra/rate-limit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { ServiceError } from 'errors/index.js';

async function check(request) {
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
if (['test', 'development'].includes(process.env.NODE_ENV) || process.env.CI || process.env.GITHUB_ACTIONS) {
return { success: true };
}

throw new ServiceError({
message: 'Variáveis de ambiente UPSTASH_REDIS_REST_URL ou UPSTASH_REDIS_REST_TOKEN não encontradas.',
action:
'Configure as variáveis de ambiente UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN para habilitar o serviço de rate-limiting.',
context: {
upstashRedisRestUrl: process.env.UPSTASH_REDIS_REST_URL ? true : false,
upstashRedisRestToken: process.env.UPSTASH_REDIS_REST_TOKEN ? true : false,
},
errorLocationCode: 'MIDDLEWARE:RATE_LIMIT:CHECK:ENV_MISSING',
});
}

const ip = getIP(request);
const method = request.method;
const path = request.nextUrl.pathname;
const generalIdentifier = `${ip}`;
const specificIdentifier = `${ip}:${method}:${path}`;
const limits = getLimits();

try {
if (method === 'POST' && path === '/api/v1/sessions') {
const rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(limits.postSessions.requests, limits.postSessions.window),
});

return await rateLimit.limit(specificIdentifier);
}

const generalRateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(limits.general.requests, limits.general.window),
});

return await generalRateLimit.limit(generalIdentifier);
} catch (error) {
throw new ServiceError({
message: error.message,
action: 'Verifique se o serviço Upstash está disponível.',
stack: error.stack,
context: {
ip,
method,
path,
},
errorLocationCode: 'MIDDLEWARE:RATE_LIMIT:CHECK',
});
}
}

function getIP(request) {
const xff = request instanceof Request ? request.headers.get('x-forwarded-for') : request.headers['x-forwarded-for'];

return xff ? (Array.isArray(xff) ? xff[0] : xff.split(',')[0]) : '127.0.0.1';
}

function getLimits() {
const defaultLimits = {
general: {
requests: 1000,
window: '5 m',
},
postSessions: {
requests: 50,
window: '30 m',
},
};

const limits = process.env.RATE_LIMITS ? JSON.parse(process.env.RATE_LIMITS) : defaultLimits;

// `process.env.RATE_LIMITS` can exist without all the keys
// so we must fill them with the default values.
limits.general ? limits.general : defaultLimits.general;
limits.postSessions ? limits.postSessions : defaultLimits.postSessions;

return limits;
}

export default Object.freeze({
check,
});
30 changes: 30 additions & 0 deletions middleware.public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import rateLimit from 'infra/rate-limit.js';
import snakeize from 'snakeize';

export const config = {
matcher: ['/api/:path*'],
};

export async function middleware(request) {
const url = request.nextUrl;

try {
const rateLimitResult = await rateLimit.check(request);

if (!rateLimitResult.success && url.pathname === '/api/v1/sessions') {
url.pathname = '/api/v1/_responses/rate-limit-reached-sessions'; // Fake response.
return NextResponse.rewrite(url);
}

if (!rateLimitResult.success) {
url.pathname = '/api/v1/_responses/rate-limit-reached';
return NextResponse.rewrite(url);
}

return NextResponse.next();
} catch (error) {
console.error(snakeize(error));
return NextResponse.next();
}
}
2 changes: 1 addition & 1 deletion models/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ const schemas = {
return Joi.object({
body: Joi.string()
.min(1)
.max(20000)
.max(16000)
.trim()
.invalid(null)
.when('$required.body', { is: 'required', then: Joi.required(), otherwise: Joi.optional() })
Expand Down
Loading

0 comments on commit 6f82ceb

Please sign in to comment.