feat: improve ux and security
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { env } from "#server/env";
|
||||
|
||||
@@ -6,10 +7,44 @@ const bodySchema = z.object({
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
const attempts = new Map<string, { count: number; resetAt: number }>();
|
||||
const MAX_ATTEMPTS = 10;
|
||||
const WINDOW_MS = 60_000;
|
||||
|
||||
const isRateLimited = (ip: string): boolean => {
|
||||
const now = Date.now();
|
||||
const entry = attempts.get(ip);
|
||||
|
||||
if (!entry || now > entry.resetAt) {
|
||||
attempts.set(ip, { count: 1, resetAt: now + WINDOW_MS });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.count >= MAX_ATTEMPTS) return true;
|
||||
entry.count++;
|
||||
return false;
|
||||
};
|
||||
|
||||
const safeEqual = (a: string, b: string): boolean => {
|
||||
const aBuf = Buffer.from(a);
|
||||
const bBuf = Buffer.from(b);
|
||||
if (aBuf.length !== bBuf.length) {
|
||||
timingSafeEqual(aBuf, aBuf);
|
||||
return false;
|
||||
}
|
||||
return timingSafeEqual(aBuf, bBuf);
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const ip = getRequestIP(event) ?? "unknown";
|
||||
if (isRateLimited(ip)) {
|
||||
throw createError({ statusCode: 429, message: "Too many attempts, try again later" });
|
||||
}
|
||||
|
||||
const body = await readValidatedBody(event, bodySchema.parse);
|
||||
|
||||
if (body.username !== env.ADMIN_USERNAME || body.password !== env.ADMIN_PASSWORD) {
|
||||
const valid = safeEqual(body.username, env.ADMIN_USERNAME) && safeEqual(body.password, env.ADMIN_PASSWORD);
|
||||
if (!valid) {
|
||||
throw createError({ statusCode: 401, message: "Invalid credentials" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user