feat: improve ux and security

This commit is contained in:
2026-03-25 17:30:49 +01:00
parent fedb0ae8db
commit 9c61d7561a
8 changed files with 80 additions and 15 deletions

View File

@@ -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" });
}

View File

@@ -7,12 +7,16 @@ const paramsSchema = z.object({
id: z.string().transform(Number),
});
const bodySchema = z.object({
name: z.string().min(1).optional(),
path: z.string().min(1).optional(),
url: z.url().optional(),
disabled: z.boolean().optional(),
});
const bodySchema = z
.object({
name: z.string().min(1).optional(),
path: z.string().min(1).startsWith("/").optional(),
url: z.url().optional(),
disabled: z.boolean().optional(),
})
.refine((data) => Object.values(data).some((v) => v !== undefined), {
message: "At least one field must be provided",
});
export default defineEventHandler(async (event) => {
const params = await getValidatedRouterParams(event, paramsSchema.parse);

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
const bodySchema = z.object({
name: z.string().min(1),
path: z.string().min(1),
path: z.string().min(1).startsWith("/"),
url: z.url(),
disabled: z.boolean().optional(),
});