api9nin / src /app /api /auth /login /route.js
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
3.47 kB
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/localDb";
import bcrypt from "bcryptjs";
import { cookies } from "next/headers";
import { setDashboardAuthCookie } from "@/lib/auth/dashboardSession";
import { isOidcConfigured } from "@/lib/auth/oidc";
import { checkLock, recordFail, recordSuccess, getClientIp } from "@/lib/auth/loginLimiter";
import { isLocalRequest } from "@/dashboardGuard";
const RESET_HINT = "Forgot password? Reset to default via 9Router CLI β†’ Settings β†’ Reset Password to Default.";
function isTunnelRequest(request, settings) {
const host = (request.headers.get("host") || "").split(":")[0].toLowerCase();
const tunnelHost = settings.tunnelUrl ? new URL(settings.tunnelUrl).hostname.toLowerCase() : "";
const tailscaleHost = settings.tailscaleUrl ? new URL(settings.tailscaleUrl).hostname.toLowerCase() : "";
return (tunnelHost && host === tunnelHost) || (tailscaleHost && host === tailscaleHost);
}
export async function POST(request) {
try {
const ip = getClientIp(request);
const lock = checkLock(ip);
if (lock.locked) {
return NextResponse.json(
{ error: `Too many failed attempts. Try again in ${lock.retryAfter}s. ${RESET_HINT}`, retryAfter: lock.retryAfter, resetHint: RESET_HINT },
{ status: 429, headers: { "Retry-After": String(lock.retryAfter) } }
);
}
const { password } = await request.json();
const settings = await getSettings();
// Block login via tunnel/tailscale if dashboard access is disabled
if (isTunnelRequest(request, settings) && settings.tunnelDashboardAccess !== true) {
return NextResponse.json({ error: "Dashboard access via tunnel is disabled" }, { status: 403 });
}
// Default password is '123456' if not set
const storedHash = settings.password;
if (settings.authMode === "oidc" && isOidcConfigured(settings)) {
return NextResponse.json({ error: "Password login is disabled. Use OIDC sign in." }, { status: 403 });
}
let isValid = false;
if (storedHash) {
isValid = await bcrypt.compare(password, storedHash);
} else {
// Use env var or default
const initialPassword = process.env.INITIAL_PASSWORD || "123456";
isValid = password === initialPassword;
}
if (isValid) {
recordSuccess(ip);
const cookieStore = await cookies();
await setDashboardAuthCookie(cookieStore, request);
// Default password still in use on a remote client β†’ force a password
// change before the dashboard is exposed remotely (keeps local UX intact).
const mustChangePassword =
!storedHash && !process.env.INITIAL_PASSWORD && !isLocalRequest(request);
return NextResponse.json({ success: true, mustChangePassword });
}
const { remainingBeforeLock } = recordFail(ip);
const postLock = checkLock(ip);
if (postLock.locked) {
return NextResponse.json(
{ error: `Too many failed attempts. Try again in ${postLock.retryAfter}s. ${RESET_HINT}`, retryAfter: postLock.retryAfter, resetHint: RESET_HINT },
{ status: 429, headers: { "Retry-After": String(postLock.retryAfter) } }
);
}
return NextResponse.json(
{ error: `Invalid password. ${remainingBeforeLock} attempt(s) left before lockout.`, remainingBeforeLock },
{ status: 401 }
);
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}