feat: improve learning and platform operations
All checks were successful
CI / Validate frontend and API (push) Successful in 45s
CI / Build and publish Docker images (push) Successful in 24s

This commit is contained in:
Cauê Faleiros
2026-09-01 15:38:14 -03:00
parent 4736fb5208
commit 3d4c72e85c
20 changed files with 292 additions and 45 deletions

View File

@@ -4,6 +4,7 @@ import { hashPassword, verifyPassword } from '../auth/passwords.js';
import { hashToken } from '../auth/account-tokens.js';
import type { AuthUser } from '../auth/plugin.js';
import { pool } from '../db/pool.js';
import { config } from '../config.js';
const credentialsSchema = z.object({
email: z.string().email().transform((email) => email.toLowerCase()),
@@ -32,7 +33,27 @@ const serializeUser = (user: UserRow): AuthUser => ({
});
export const authRoutes: FastifyPluginAsync = async (app) => {
const publicAttempts = new Map<string, number[]>();
const allowPublicAuthAttempt = (ip: string) => {
const now = Date.now();
const earliestAllowed = now - config.AUTH_RATE_LIMIT_WINDOW_SECONDS * 1000;
const attempts = (publicAttempts.get(ip) || []).filter((timestamp) => timestamp > earliestAllowed);
if (attempts.length >= config.AUTH_RATE_LIMIT_MAX) {
publicAttempts.set(ip, attempts);
return false;
}
attempts.push(now);
publicAttempts.set(ip, attempts);
return true;
};
const rejectIfRateLimited = (ip: string, reply: { code: (status: number) => { send: (payload: object) => unknown } }) => {
if (allowPublicAuthAttempt(ip)) return false;
reply.code(429).send({ error: 'Too many attempts. Please try again later.' });
return true;
};
app.post('/accept-invitation', async (request, reply) => {
if (rejectIfRateLimited(request.ip, reply)) return;
const input = tokenPasswordSchema.extend({ name: z.string().trim().min(2).max(120) }).parse(request.body);
const client = await pool.connect();
try {
@@ -63,6 +84,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
});
app.post('/reset-password', async (request, reply) => {
if (rejectIfRateLimited(request.ip, reply)) return;
const input = tokenPasswordSchema.parse(request.body);
const client = await pool.connect();
try {
@@ -87,6 +109,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
}
});
app.post('/register', async (request, reply) => {
if (rejectIfRateLimited(request.ip, reply)) return;
const input = registerSchema.parse(request.body);
const passwordHash = await hashPassword(input.password);
@@ -109,6 +132,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
});
app.post('/login', async (request, reply) => {
if (rejectIfRateLimited(request.ip, reply)) return;
const input = credentialsSchema.parse(request.body);
const result = await pool.query<UserRow>(
`select id, email, display_name, role, password_hash, is_active from users where email = $1`,