mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Implement auth ip rate-limits as fastify plugin (#20)
This commit is contained in:
@@ -3,7 +3,7 @@ import fp from 'fastify-plugin';
|
|||||||
import { ApiErrorCode } from '@colanode/core';
|
import { ApiErrorCode } from '@colanode/core';
|
||||||
|
|
||||||
import { parseToken, verifyToken } from '@/lib/tokens';
|
import { parseToken, verifyToken } from '@/lib/tokens';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
import { isDeviceApiRateLimited } from '@/lib/rate-limits';
|
||||||
import { RequestAccount } from '@/types/api';
|
import { RequestAccount } from '@/types/api';
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
@@ -48,9 +48,7 @@ const accountAuthenticatorCallback: FastifyPluginCallback = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRateLimited = await rateLimitService.isDeviceApiRateLimitted(
|
const isRateLimited = await isDeviceApiRateLimited(tokenData.deviceId);
|
||||||
tokenData.deviceId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isRateLimited) {
|
if (isRateLimited) {
|
||||||
return reply.code(429).send({
|
return reply.code(429).send({
|
||||||
|
|||||||
23
apps/server/src/api/client/plugins/auth-ip-rate-limit.ts
Normal file
23
apps/server/src/api/client/plugins/auth-ip-rate-limit.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { FastifyPluginCallback } from 'fastify';
|
||||||
|
import fp from 'fastify-plugin';
|
||||||
|
import { ApiErrorCode } from '@colanode/core';
|
||||||
|
|
||||||
|
import { isAuthIpRateLimited } from '@/lib/rate-limits';
|
||||||
|
|
||||||
|
const authIpRateLimiterCallback: FastifyPluginCallback = (fastify, _, done) => {
|
||||||
|
fastify.addHook('onRequest', async (request, reply) => {
|
||||||
|
const ip = request.client.ip;
|
||||||
|
const isIpRateLimited = await isAuthIpRateLimited(ip);
|
||||||
|
|
||||||
|
if (isIpRateLimited) {
|
||||||
|
return reply.code(429).send({
|
||||||
|
code: ApiErrorCode.TooManyRequests,
|
||||||
|
message: 'Too many authentication attempts. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authIpRateLimiter = fp(authIpRateLimiterCallback);
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
AccountUpdateInput,
|
|
||||||
accountUpdateInputSchema,
|
accountUpdateInputSchema,
|
||||||
AccountUpdateOutput,
|
AccountUpdateOutput,
|
||||||
accountUpdateOutputSchema,
|
accountUpdateOutputSchema,
|
||||||
@@ -34,7 +33,7 @@ export const accountUpdateRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const accountId = request.params.accountId;
|
const accountId = request.params.accountId;
|
||||||
const input = request.body as AccountUpdateInput;
|
const input = request.body;
|
||||||
|
|
||||||
if (accountId !== request.account.id) {
|
if (accountId !== request.account.id) {
|
||||||
return reply.code(400).send({
|
return reply.code(400).send({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
import { isAuthEmailRateLimited } from '@/lib/rate-limits';
|
||||||
import { configuration } from '@/lib/configuration';
|
import { configuration } from '@/lib/configuration';
|
||||||
import {
|
import {
|
||||||
buildLoginSuccessOutput,
|
buildLoginSuccessOutput,
|
||||||
@@ -34,20 +34,10 @@ export const emailLoginRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const ip = request.client.ip;
|
|
||||||
const isIpRateLimited = await rateLimitService.isAuthIpRateLimitted(ip);
|
|
||||||
if (isIpRateLimited) {
|
|
||||||
return reply.code(429).send({
|
|
||||||
code: ApiErrorCode.TooManyRequests,
|
|
||||||
message: 'Too many authentication attempts. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = request.body;
|
const input = request.body;
|
||||||
const email = input.email.toLowerCase();
|
const email = input.email.toLowerCase();
|
||||||
|
|
||||||
const isEmailRateLimited =
|
const isEmailRateLimited = await isAuthEmailRateLimited(email);
|
||||||
await rateLimitService.isAuthEmailRateLimitted(email);
|
|
||||||
if (isEmailRateLimited) {
|
if (isEmailRateLimited) {
|
||||||
return reply.code(429).send({
|
return reply.code(429).send({
|
||||||
code: ApiErrorCode.TooManyRequests,
|
code: ApiErrorCode.TooManyRequests,
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {
|
|||||||
AccountStatus,
|
AccountStatus,
|
||||||
ApiErrorCode,
|
ApiErrorCode,
|
||||||
apiErrorOutputSchema,
|
apiErrorOutputSchema,
|
||||||
EmailPasswordResetCompleteInput,
|
emailPasswordResetCompleteInputSchema,
|
||||||
EmailPasswordResetCompleteOutput,
|
EmailPasswordResetCompleteOutput,
|
||||||
emailPasswordResetCompleteOutputSchema,
|
emailPasswordResetCompleteOutputSchema,
|
||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { generatePasswordHash, verifyOtpCode } from '@/lib/accounts';
|
import { generatePasswordHash, verifyOtpCode } from '@/lib/accounts';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
|
||||||
|
|
||||||
export const emailPasswordResetCompleteRoute: FastifyPluginCallbackZod = (
|
export const emailPasswordResetCompleteRoute: FastifyPluginCallbackZod = (
|
||||||
instance,
|
instance,
|
||||||
@@ -21,6 +20,7 @@ export const emailPasswordResetCompleteRoute: FastifyPluginCallbackZod = (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/emails/passwords/reset/complete',
|
url: '/emails/passwords/reset/complete',
|
||||||
schema: {
|
schema: {
|
||||||
|
body: emailPasswordResetCompleteInputSchema,
|
||||||
response: {
|
response: {
|
||||||
200: emailPasswordResetCompleteOutputSchema,
|
200: emailPasswordResetCompleteOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
@@ -29,16 +29,7 @@ export const emailPasswordResetCompleteRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const ip = request.client.ip;
|
const input = request.body;
|
||||||
const isIpRateLimited = await rateLimitService.isAuthIpRateLimitted(ip);
|
|
||||||
if (isIpRateLimited) {
|
|
||||||
return reply.code(429).send({
|
|
||||||
code: ApiErrorCode.TooManyRequests,
|
|
||||||
message: 'Too many authentication attempts. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = request.body as EmailPasswordResetCompleteInput;
|
|
||||||
const accountId = await verifyOtpCode(input.id, input.otp);
|
const accountId = await verifyOtpCode(input.id, input.otp);
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import {
|
|||||||
generateId,
|
generateId,
|
||||||
IdType,
|
IdType,
|
||||||
ApiErrorCode,
|
ApiErrorCode,
|
||||||
EmailPasswordResetInitInput,
|
|
||||||
EmailPasswordResetInitOutput,
|
EmailPasswordResetInitOutput,
|
||||||
apiErrorOutputSchema,
|
apiErrorOutputSchema,
|
||||||
emailPasswordResetInitOutputSchema,
|
emailPasswordResetInitOutputSchema,
|
||||||
|
emailPasswordResetInitInputSchema,
|
||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
import { isAuthEmailRateLimited } from '@/lib/rate-limits';
|
||||||
import { configuration } from '@/lib/configuration';
|
import { configuration } from '@/lib/configuration';
|
||||||
import { generateOtpCode, saveOtp } from '@/lib/otps';
|
import { generateOtpCode, saveOtp } from '@/lib/otps';
|
||||||
import { AccountPasswordResetOtpAttributes, Otp } from '@/types/otps';
|
import { AccountPasswordResetOtpAttributes, Otp } from '@/types/otps';
|
||||||
@@ -25,6 +25,7 @@ export const emailPasswordResetInitRoute: FastifyPluginCallbackZod = (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/emails/passwords/reset/init',
|
url: '/emails/passwords/reset/init',
|
||||||
schema: {
|
schema: {
|
||||||
|
body: emailPasswordResetInitInputSchema,
|
||||||
response: {
|
response: {
|
||||||
200: emailPasswordResetInitOutputSchema,
|
200: emailPasswordResetInitOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
@@ -32,20 +33,10 @@ export const emailPasswordResetInitRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const ip = request.client.ip;
|
const input = request.body;
|
||||||
const isIpRateLimited = await rateLimitService.isAuthIpRateLimitted(ip);
|
|
||||||
if (isIpRateLimited) {
|
|
||||||
return reply.code(429).send({
|
|
||||||
code: ApiErrorCode.TooManyRequests,
|
|
||||||
message: 'Too many authentication attempts. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = request.body as EmailPasswordResetInitInput;
|
|
||||||
const email = input.email.toLowerCase();
|
const email = input.email.toLowerCase();
|
||||||
|
|
||||||
const isEmailRateLimited =
|
const isEmailRateLimited = await isAuthEmailRateLimited(email);
|
||||||
await rateLimitService.isAuthEmailRateLimitted(email);
|
|
||||||
if (isEmailRateLimited) {
|
if (isEmailRateLimited) {
|
||||||
return reply.code(429).send({
|
return reply.code(429).send({
|
||||||
code: ApiErrorCode.TooManyRequests,
|
code: ApiErrorCode.TooManyRequests,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
||||||
import {
|
import {
|
||||||
AccountStatus,
|
AccountStatus,
|
||||||
EmailRegisterInput,
|
|
||||||
generateId,
|
generateId,
|
||||||
IdType,
|
IdType,
|
||||||
ApiErrorCode,
|
ApiErrorCode,
|
||||||
apiErrorOutputSchema,
|
apiErrorOutputSchema,
|
||||||
loginOutputSchema,
|
loginOutputSchema,
|
||||||
|
emailRegisterInputSchema,
|
||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { SelectAccount } from '@/data/schema';
|
import { SelectAccount } from '@/data/schema';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
import { isAuthEmailRateLimited } from '@/lib/rate-limits';
|
||||||
import { configuration } from '@/lib/configuration';
|
import { configuration } from '@/lib/configuration';
|
||||||
import {
|
import {
|
||||||
buildLoginSuccessOutput,
|
buildLoginSuccessOutput,
|
||||||
@@ -28,6 +28,7 @@ export const emailRegisterRoute: FastifyPluginCallbackZod = (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/emails/register',
|
url: '/emails/register',
|
||||||
schema: {
|
schema: {
|
||||||
|
body: emailRegisterInputSchema,
|
||||||
response: {
|
response: {
|
||||||
200: loginOutputSchema,
|
200: loginOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
@@ -35,20 +36,10 @@ export const emailRegisterRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const ip = request.client.ip;
|
const input = request.body;
|
||||||
const isIpRateLimited = await rateLimitService.isAuthIpRateLimitted(ip);
|
|
||||||
if (isIpRateLimited) {
|
|
||||||
return reply.code(429).send({
|
|
||||||
code: ApiErrorCode.TooManyRequests,
|
|
||||||
message: 'Too many authentication attempts. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = request.body as EmailRegisterInput;
|
|
||||||
const email = input.email.toLowerCase();
|
const email = input.email.toLowerCase();
|
||||||
|
|
||||||
const isEmailRateLimited =
|
const isEmailRateLimited = await isAuthEmailRateLimited(email);
|
||||||
await rateLimitService.isAuthEmailRateLimitted(email);
|
|
||||||
if (isEmailRateLimited) {
|
if (isEmailRateLimited) {
|
||||||
return reply.code(429).send({
|
return reply.code(429).send({
|
||||||
code: ApiErrorCode.TooManyRequests,
|
code: ApiErrorCode.TooManyRequests,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
AccountStatus,
|
AccountStatus,
|
||||||
ApiErrorCode,
|
ApiErrorCode,
|
||||||
apiErrorOutputSchema,
|
apiErrorOutputSchema,
|
||||||
EmailVerifyInput,
|
emailVerifyInputSchema,
|
||||||
loginOutputSchema,
|
loginOutputSchema,
|
||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ export const emailVerifyRoute: FastifyPluginCallbackZod = (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/emails/verify',
|
url: '/emails/verify',
|
||||||
schema: {
|
schema: {
|
||||||
|
body: emailVerifyInputSchema,
|
||||||
response: {
|
response: {
|
||||||
200: loginOutputSchema,
|
200: loginOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
@@ -26,7 +27,7 @@ export const emailVerifyRoute: FastifyPluginCallbackZod = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (request, reply) => {
|
handler: async (request, reply) => {
|
||||||
const input = request.body as EmailVerifyInput;
|
const input = request.body;
|
||||||
const accountId = await verifyOtpCode(input.id, input.otp);
|
const accountId = await verifyOtpCode(input.id, input.otp);
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
|
|||||||
@@ -2,23 +2,22 @@ import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
|
|||||||
import {
|
import {
|
||||||
AccountStatus,
|
AccountStatus,
|
||||||
generateId,
|
generateId,
|
||||||
GoogleLoginInput,
|
|
||||||
GoogleUserInfo,
|
GoogleUserInfo,
|
||||||
IdType,
|
IdType,
|
||||||
ApiErrorCode,
|
ApiErrorCode,
|
||||||
apiErrorOutputSchema,
|
apiErrorOutputSchema,
|
||||||
loginOutputSchema,
|
loginOutputSchema,
|
||||||
|
googleLoginInputSchema,
|
||||||
} from '@colanode/core';
|
} from '@colanode/core';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { database } from '@/data/database';
|
import { database } from '@/data/database';
|
||||||
import { rateLimitService } from '@/services/rate-limit-service';
|
|
||||||
import { configuration } from '@/lib/configuration';
|
import { configuration } from '@/lib/configuration';
|
||||||
import { buildLoginSuccessOutput } from '@/lib/accounts';
|
import { buildLoginSuccessOutput } from '@/lib/accounts';
|
||||||
|
|
||||||
const GoogleUserInfoUrl = 'https://www.googleapis.com/oauth2/v1/userinfo';
|
const GoogleUserInfoUrl = 'https://www.googleapis.com/oauth2/v1/userinfo';
|
||||||
|
|
||||||
export const loginWithGoogleRoute: FastifyPluginCallbackZod = (
|
export const googleLoginRoute: FastifyPluginCallbackZod = (
|
||||||
instance,
|
instance,
|
||||||
_,
|
_,
|
||||||
done
|
done
|
||||||
@@ -27,6 +26,7 @@ export const loginWithGoogleRoute: FastifyPluginCallbackZod = (
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/google/login',
|
url: '/google/login',
|
||||||
schema: {
|
schema: {
|
||||||
|
body: googleLoginInputSchema,
|
||||||
response: {
|
response: {
|
||||||
200: loginOutputSchema,
|
200: loginOutputSchema,
|
||||||
400: apiErrorOutputSchema,
|
400: apiErrorOutputSchema,
|
||||||
@@ -41,16 +41,7 @@ export const loginWithGoogleRoute: FastifyPluginCallbackZod = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ip = request.client.ip;
|
const input = request.body;
|
||||||
const isIpRateLimited = await rateLimitService.isAuthIpRateLimitted(ip);
|
|
||||||
if (isIpRateLimited) {
|
|
||||||
return reply.code(429).send({
|
|
||||||
code: ApiErrorCode.TooManyRequests,
|
|
||||||
message: 'Too many authentication attempts. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = request.body as GoogleLoginInput;
|
|
||||||
const url = `${GoogleUserInfoUrl}?access_token=${input.access_token}`;
|
const url = `${GoogleUserInfoUrl}?access_token=${input.access_token}`;
|
||||||
const userInfoResponse = await axios.get(url);
|
const userInfoResponse = await axios.get(url);
|
||||||
|
|
||||||
@@ -2,30 +2,35 @@ import { FastifyPluginCallback } from 'fastify';
|
|||||||
|
|
||||||
import { accountSyncRoute } from './account-sync';
|
import { accountSyncRoute } from './account-sync';
|
||||||
import { emailLoginRoute } from './email-login';
|
import { emailLoginRoute } from './email-login';
|
||||||
import { loginWithGoogleRoute } from './login-google';
|
|
||||||
import { logoutRoute } from './logout';
|
import { logoutRoute } from './logout';
|
||||||
import { emailRegisterRoute } from './email-register';
|
import { emailRegisterRoute } from './email-register';
|
||||||
import { accountUpdateRoute } from './account-update';
|
import { accountUpdateRoute } from './account-update';
|
||||||
import { emailVerifyRoute } from './email-verify';
|
import { emailVerifyRoute } from './email-verify';
|
||||||
import { emailPasswordResetInitRoute } from './email-password-reset-init';
|
import { emailPasswordResetInitRoute } from './email-password-reset-init';
|
||||||
import { emailPasswordResetCompleteRoute } from './email-password-reset-complete';
|
import { emailPasswordResetCompleteRoute } from './email-password-reset-complete';
|
||||||
|
import { googleLoginRoute } from './google-login';
|
||||||
|
|
||||||
import { accountAuthenticator } from '@/api/client/plugins/account-auth';
|
import { accountAuthenticator } from '@/api/client/plugins/account-auth';
|
||||||
|
import { authIpRateLimiter } from '@/api/client/plugins/auth-ip-rate-limit';
|
||||||
|
|
||||||
export const accountRoutes: FastifyPluginCallback = (instance, _, done) => {
|
export const accountRoutes: FastifyPluginCallback = (instance, _, done) => {
|
||||||
instance.register(emailLoginRoute);
|
instance.register((subInstance) => {
|
||||||
instance.register(loginWithGoogleRoute);
|
subInstance.register(authIpRateLimiter);
|
||||||
instance.register(emailRegisterRoute);
|
|
||||||
instance.register(emailVerifyRoute);
|
|
||||||
instance.register(emailPasswordResetInitRoute);
|
|
||||||
instance.register(emailPasswordResetCompleteRoute);
|
|
||||||
|
|
||||||
instance.register(async (subInstance) => {
|
subInstance.register(emailLoginRoute);
|
||||||
await subInstance.register(accountAuthenticator);
|
subInstance.register(emailRegisterRoute);
|
||||||
|
subInstance.register(emailVerifyRoute);
|
||||||
|
subInstance.register(emailPasswordResetInitRoute);
|
||||||
|
subInstance.register(emailPasswordResetCompleteRoute);
|
||||||
|
subInstance.register(googleLoginRoute);
|
||||||
|
});
|
||||||
|
|
||||||
await subInstance.register(accountSyncRoute);
|
instance.register((subInstance) => {
|
||||||
await subInstance.register(logoutRoute);
|
subInstance.register(accountAuthenticator);
|
||||||
await subInstance.register(accountUpdateRoute);
|
|
||||||
|
subInstance.register(accountSyncRoute);
|
||||||
|
subInstance.register(accountUpdateRoute);
|
||||||
|
subInstance.register(logoutRoute);
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|||||||
63
apps/server/src/lib/rate-limits.ts
Normal file
63
apps/server/src/lib/rate-limits.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { sha256 } from 'js-sha256';
|
||||||
|
|
||||||
|
import { redis } from '@/data/redis';
|
||||||
|
|
||||||
|
interface RateLimitConfig {
|
||||||
|
limit: number;
|
||||||
|
window: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: RateLimitConfig = {
|
||||||
|
limit: 10,
|
||||||
|
window: 300, // 5 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRateLimited = async (
|
||||||
|
key: string,
|
||||||
|
config: RateLimitConfig = defaultConfig
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const redisKey = `rt:${key}`;
|
||||||
|
const attempts = await redis.incr(redisKey);
|
||||||
|
|
||||||
|
// Set expiry on first attempt
|
||||||
|
if (attempts === 1) {
|
||||||
|
await redis.expire(redisKey, config.window);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attempts > config.limit;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAuthIpRateLimited = async (ip: string): Promise<boolean> => {
|
||||||
|
return await isRateLimited(`ai:${ip}`, {
|
||||||
|
limit: 100,
|
||||||
|
window: 600, // 10 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAuthEmailRateLimited = async (
|
||||||
|
email: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const emailHash = sha256(email);
|
||||||
|
return await isRateLimited(`ae:${emailHash}`, {
|
||||||
|
limit: 10,
|
||||||
|
window: 600, // 10 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDeviceApiRateLimited = async (
|
||||||
|
deviceId: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
return await isRateLimited(`da:${deviceId}`, {
|
||||||
|
limit: 100,
|
||||||
|
window: 60, // 1 minute
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDeviceSocketRateLimited = async (
|
||||||
|
deviceId: string
|
||||||
|
): Promise<boolean> => {
|
||||||
|
return await isRateLimited(`ds:${deviceId}`, {
|
||||||
|
limit: 20,
|
||||||
|
window: 60, // 1 minute
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { sha256 } from 'js-sha256';
|
|
||||||
|
|
||||||
import { redis } from '@/data/redis';
|
|
||||||
|
|
||||||
interface RateLimitConfig {
|
|
||||||
limit: number;
|
|
||||||
window: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RateLimitService {
|
|
||||||
private defaultConfig: RateLimitConfig = {
|
|
||||||
limit: 10,
|
|
||||||
window: 300, // 5 minutes
|
|
||||||
};
|
|
||||||
|
|
||||||
public async isAuthIpRateLimitted(ip: string): Promise<boolean> {
|
|
||||||
return await this.isRateLimited(`ai:${ip}`, {
|
|
||||||
limit: 100,
|
|
||||||
window: 600, // 10 minutes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isAuthEmailRateLimitted(email: string): Promise<boolean> {
|
|
||||||
const emailHash = sha256(email);
|
|
||||||
return await this.isRateLimited(`ae:${emailHash}`, {
|
|
||||||
limit: 10,
|
|
||||||
window: 600, // 10 minutes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isDeviceApiRateLimitted(deviceId: string): Promise<boolean> {
|
|
||||||
return await this.isRateLimited(`da:${deviceId}`, {
|
|
||||||
limit: 100,
|
|
||||||
window: 60, // 1 minute
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isDeviceSocketRateLimitted(deviceId: string): Promise<boolean> {
|
|
||||||
return await this.isRateLimited(`ds:${deviceId}`, {
|
|
||||||
limit: 20,
|
|
||||||
window: 60, // 1 minute
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async isRateLimited(
|
|
||||||
key: string,
|
|
||||||
config: RateLimitConfig = this.defaultConfig
|
|
||||||
): Promise<boolean> {
|
|
||||||
const redisKey = `rt:${key}`;
|
|
||||||
const attempts = await redis.incr(redisKey);
|
|
||||||
|
|
||||||
// Set expiry on first attempt
|
|
||||||
if (attempts === 1) {
|
|
||||||
await redis.expire(redisKey, config.window);
|
|
||||||
}
|
|
||||||
|
|
||||||
return attempts > config.limit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rateLimitService = new RateLimitService();
|
|
||||||
Reference in New Issue
Block a user