Files
sass-mohfarawati/lib/admin-auth.ts
T
MOH e32ac4cacb
CI / quality (push) Waiting to run
Upgrade app to Next.js 16
2026-03-13 15:50:37 +01:00

173 lines
4.2 KiB
TypeScript

import { createHmac, timingSafeEqual } from "crypto";
import { cookies } from "next/headers";
export const ADMIN_SESSION_COOKIE = "moh_admin_session";
const ADMIN_FAIL_COOKIE = "moh_admin_fail";
const ADMIN_SESSION_VALUE = "superadmin";
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_SECONDS = 15 * 60;
function getSecret(): string {
return process.env.ADMIN_AUTH_SECRET ?? "";
}
function getPassword(): string {
return process.env.ADMIN_PASSWORD ?? "";
}
function signValue(value: string): string {
return createHmac("sha256", getSecret()).update(value).digest("hex");
}
function buildToken(): string {
return `${ADMIN_SESSION_VALUE}.${signValue(ADMIN_SESSION_VALUE)}`;
}
function verifyToken(token: string): boolean {
const parts = token.split(".");
if (parts.length !== 2) {
return false;
}
const [value, signature] = parts;
if (value !== ADMIN_SESSION_VALUE) {
return false;
}
const expected = signValue(value);
const left = Buffer.from(signature);
const right = Buffer.from(expected);
if (left.length !== right.length) {
return false;
}
return timingSafeEqual(left, right);
}
export function isAdminAuthConfigured(): boolean {
return getPassword().length > 0 && getSecret().length > 0;
}
export function isPasswordValid(password: string): boolean {
if (!isAdminAuthConfigured()) {
return false;
}
const provided = Buffer.from(password);
const expected = Buffer.from(getPassword());
if (provided.length !== expected.length) {
return false;
}
return timingSafeEqual(provided, expected);
}
export async function setAdminSessionCookie(): Promise<void> {
const store = await cookies();
store.set(ADMIN_SESSION_COOKIE, buildToken(), {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 8,
});
}
export async function clearAdminSessionCookie(): Promise<void> {
const store = await cookies();
store.set(ADMIN_SESSION_COOKIE, "", {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 0,
});
}
type FailState = {
attempts: number;
lockUntil: number;
};
function parseFailState(rawValue: string | undefined): FailState {
if (!rawValue) {
return { attempts: 0, lockUntil: 0 };
}
try {
const parsed = JSON.parse(rawValue) as Partial<FailState>;
return {
attempts: Number(parsed.attempts ?? 0),
lockUntil: Number(parsed.lockUntil ?? 0),
};
} catch {
return { attempts: 0, lockUntil: 0 };
}
}
export async function getAdminLockState(): Promise<{ locked: boolean; remainingSeconds: number }> {
const store = await cookies();
const state = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
const now = Date.now();
if (state.lockUntil > now) {
return {
locked: true,
remainingSeconds: Math.ceil((state.lockUntil - now) / 1000),
};
}
return { locked: false, remainingSeconds: 0 };
}
export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; remainingSeconds: number }> {
const store = await cookies();
const now = Date.now();
const current = parseFailState(store.get(ADMIN_FAIL_COOKIE)?.value);
const attempts = current.lockUntil > now ? current.attempts : current.attempts + 1;
const locked = attempts >= MAX_FAILED_ATTEMPTS;
const lockUntil = locked ? now + LOCKOUT_SECONDS * 1000 : 0;
store.set(ADMIN_FAIL_COOKIE, JSON.stringify({ attempts, lockUntil }), {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: LOCKOUT_SECONDS,
});
return {
locked,
remainingSeconds: locked ? LOCKOUT_SECONDS : 0,
};
}
export async function resetAdminFailedAttempts(): Promise<void> {
const store = await cookies();
store.set(ADMIN_FAIL_COOKIE, "", {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 0,
});
}
export async function isAdminAuthenticated(): Promise<boolean> {
if (!isAdminAuthConfigured()) {
return false;
}
const store = await cookies();
const token = store.get(ADMIN_SESSION_COOKIE)?.value;
if (!token) {
return false;
}
return verifyToken(token);
}