31 lines
1.2 KiB
TypeScript
31 lines
1.2 KiB
TypeScript
import { AppError } from '../common/errors/app-error';
|
|
import { Injectable } from '@nestjs/common';
|
|
import { DatabaseService } from '../database/database.service';
|
|
import { hashToken } from './tokens';
|
|
|
|
@Injectable()
|
|
export class RateLimitService {
|
|
constructor(private readonly db: DatabaseService) {}
|
|
async consume(key: string, limit: number, seconds: number): Promise<void> {
|
|
const digest = hashToken(key);
|
|
const [row] = await this.db.$queryRaw<
|
|
Array<{ hits: number; expiresAt: Date }>
|
|
>`
|
|
INSERT INTO rate_limits (key, hits, expires_at)
|
|
VALUES (${digest}, 1, NOW() + ${seconds} * INTERVAL '1 second')
|
|
ON CONFLICT (key) DO UPDATE SET
|
|
hits = CASE WHEN rate_limits.expires_at <= NOW() THEN 1 ELSE rate_limits.hits + 1 END,
|
|
expires_at = CASE WHEN rate_limits.expires_at <= NOW()
|
|
THEN NOW() + ${seconds} * INTERVAL '1 second' ELSE rate_limits.expires_at END
|
|
RETURNING hits, expires_at AS "expiresAt"
|
|
`;
|
|
if (row.hits > limit)
|
|
throw new AppError(
|
|
'RATE_LIMITED',
|
|
key.split(':')[0],
|
|
undefined,
|
|
Math.max(1, Math.ceil((row.expiresAt.getTime() - Date.now()) / 1000)),
|
|
);
|
|
}
|
|
}
|