| 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(); |
|
|
| |
| if (isTunnelRequest(request, settings) && settings.tunnelDashboardAccess !== true) { |
| return NextResponse.json({ error: "Dashboard access via tunnel is disabled" }, { status: 403 }); |
| } |
|
|
| |
| 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 { |
| |
| const initialPassword = process.env.INITIAL_PASSWORD || "123456"; |
| isValid = password === initialPassword; |
| } |
|
|
| if (isValid) { |
| recordSuccess(ip); |
| const cookieStore = await cookies(); |
| await setDashboardAuthCookie(cookieStore, request); |
|
|
| |
| |
| 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 }); |
| } |
| } |
|
|