feat(hardening): rate limit (signup/reset/bookings) + tâches cron + backup PostgreSQL nocturne
All checks were successful
CI / test (pull_request) Successful in 2m10s
All checks were successful
CI / test (pull_request) Successful in 2m10s
This commit is contained in:
parent
f1fb06b0af
commit
a373bd60ad
8 changed files with 319 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "@/lib/booking";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendBookingRequestToOwner, sendBookingRequestToTenant } from "@/lib/email";
|
||||
import { rateLimitRequest } from "@/lib/rate-limit";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
|
|
@ -28,6 +29,14 @@ type CreateBookingBody = {
|
|||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const rl = rateLimitRequest(request, "bookings", 60 * 60 * 1000, 10);
|
||||
if (!rl.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Trop de tentatives. Réessayez dans ${rl.retryAfter}s.` },
|
||||
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } },
|
||||
);
|
||||
}
|
||||
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
|
||||
|
|
|
|||
37
src/app/api/cron/run/[task]/route.ts
Normal file
37
src/app/api/cron/run/[task]/route.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { SCHEDULED_TASKS, type ScheduledTaskName } from "@/lib/scheduled";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function authorized(req: Request): boolean {
|
||||
const secret = (process.env.CRON_TOKEN ?? "").trim();
|
||||
if (!secret) return false;
|
||||
const header = req.headers.get("authorization") ?? "";
|
||||
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
||||
return token === secret;
|
||||
}
|
||||
|
||||
export async function POST(req: Request, ctx: { params: Promise<{ task: string }> }) {
|
||||
if (!authorized(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const { task } = await ctx.params;
|
||||
const fn = SCHEDULED_TASKS[task as ScheduledTaskName];
|
||||
if (!fn) {
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown task. Available: ${Object.keys(SCHEDULED_TASKS).join(", ")}` },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const result = await fn();
|
||||
return NextResponse.json({ ok: true, task, result });
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : String(e) },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { createPasswordResetToken } from "@/lib/password-reset";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
import { sendPasswordReset } from "@/lib/email";
|
||||
import { recordAudit } from "@/lib/admin/audit";
|
||||
import { rateLimitRequest } from "@/lib/rate-limit";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
|
|
@ -15,6 +16,13 @@ const schema = z.object({
|
|||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://karbe.cosmolan.fr";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const rl = rateLimitRequest(req, "password-reset", 60 * 60 * 1000, 3);
|
||||
if (!rl.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Trop de tentatives. Réessayez dans ${rl.retryAfter}s.` },
|
||||
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } },
|
||||
);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { hashPassword } from "@/lib/password";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
import { recordAudit } from "@/lib/admin/audit";
|
||||
import { sendSignupWelcome } from "@/lib/email";
|
||||
import { rateLimitRequest } from "@/lib/rate-limit";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
|
|
@ -19,6 +20,14 @@ const schema = z.object({
|
|||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// 5 inscriptions max par IP par heure.
|
||||
const rl = rateLimitRequest(req, "signup", 60 * 60 * 1000, 5);
|
||||
if (!rl.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Trop de tentatives. Réessayez dans ${rl.retryAfter}s.` },
|
||||
{ status: 429, headers: { "Retry-After": String(rl.retryAfter) } },
|
||||
);
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue