3fe32d923e
Build & Auto-Repair / build (push) Has been cancelled
- 86 catch blocks in 49 API route files now return the actual error message via error.message
- 14 campaign/lead/config catch blocks that lacked console.error now log errors
- 17 client pages/components now show toast.error notifications on API failures
- Silent .catch(() => {}) and finally-only try blocks now surface errors to users
151 lines
3.7 KiB
TypeScript
151 lines
3.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server"
|
|
import {
|
|
comparePassword,
|
|
getUserByEmail,
|
|
getUserByUsername,
|
|
mapDbUserToSessionUser,
|
|
recordLoginAttempt,
|
|
incrementFailedAttempts,
|
|
resetFailedAttempts,
|
|
isAccountLocked,
|
|
createSession,
|
|
setSessionContext,
|
|
SESSION_COOKIE,
|
|
} from "@/lib/auth"
|
|
import { checkRateLimit } from "@/lib/rate-limit"
|
|
|
|
function jsonResponse(data: unknown, status: number, headers?: Record<string, string>) {
|
|
return NextResponse.json(data, { status, headers })
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const ipAddress =
|
|
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
request.headers.get("x-real-ip") ||
|
|
"127.0.0.1"
|
|
|
|
const rateCheck = checkRateLimit(`login:${ipAddress}`, 10, 60000)
|
|
if (!rateCheck.allowed) {
|
|
return jsonResponse(
|
|
{ error: "Too many login attempts. Try again in 60 seconds." },
|
|
429,
|
|
{
|
|
"Retry-After": String(Math.ceil((rateCheck.resetAt - Date.now()) / 1000)),
|
|
"X-RateLimit-Remaining": "0",
|
|
},
|
|
)
|
|
}
|
|
|
|
const { email, username, password } = await request.json()
|
|
|
|
const credential = email || username
|
|
|
|
if (!credential || !password) {
|
|
return jsonResponse(
|
|
{ error: "Email/Username and password are required." },
|
|
400
|
|
)
|
|
}
|
|
|
|
if (credential.trim().length === 0 || password.trim().length === 0) {
|
|
return jsonResponse(
|
|
{ error: "Credentials cannot be empty." },
|
|
400
|
|
)
|
|
}
|
|
|
|
const userAgent = request.headers.get("user-agent") || null
|
|
|
|
// Try to find user by email first, then by username
|
|
let dbUser =
|
|
email || credential.includes("@")
|
|
? await getUserByEmail(credential)
|
|
: null
|
|
|
|
if (!dbUser) {
|
|
dbUser = await getUserByUsername(credential)
|
|
}
|
|
|
|
if (!dbUser) {
|
|
await recordLoginAttempt(
|
|
null,
|
|
credential,
|
|
ipAddress,
|
|
userAgent,
|
|
false,
|
|
"User not found"
|
|
)
|
|
return jsonResponse(
|
|
{ error: "Invalid email/username or password." },
|
|
401
|
|
)
|
|
}
|
|
|
|
const lockStatus = await isAccountLocked(dbUser)
|
|
if (lockStatus.locked) {
|
|
await recordLoginAttempt(
|
|
dbUser.id,
|
|
credential,
|
|
ipAddress,
|
|
userAgent,
|
|
false,
|
|
lockStatus.reason
|
|
)
|
|
return jsonResponse(
|
|
{ error: lockStatus.reason },
|
|
423
|
|
)
|
|
}
|
|
|
|
const valid = await comparePassword(password, dbUser.password_hash)
|
|
if (!valid) {
|
|
await incrementFailedAttempts(dbUser.id)
|
|
await recordLoginAttempt(
|
|
dbUser.id,
|
|
credential,
|
|
ipAddress,
|
|
userAgent,
|
|
false,
|
|
"Invalid password"
|
|
)
|
|
return jsonResponse(
|
|
{ error: "Invalid email/username or password." },
|
|
401
|
|
)
|
|
}
|
|
|
|
await resetFailedAttempts(dbUser.id)
|
|
await recordLoginAttempt(
|
|
dbUser.id,
|
|
credential,
|
|
ipAddress,
|
|
userAgent,
|
|
true
|
|
)
|
|
|
|
const token = await createSession(dbUser.id, dbUser.role_name)
|
|
await setSessionContext(dbUser.id, ipAddress)
|
|
|
|
const user = mapDbUserToSessionUser(dbUser)
|
|
|
|
const isHttps = request.nextUrl.protocol === "https:" || request.headers.get("x-forwarded-proto") === "https"
|
|
const response = NextResponse.json({ user })
|
|
response.cookies.set(SESSION_COOKIE, token, {
|
|
httpOnly: true,
|
|
sameSite: "strict",
|
|
path: "/",
|
|
maxAge: 86400,
|
|
secure: isHttps,
|
|
})
|
|
return response
|
|
} catch (error) {
|
|
console.error("Login error:", error)
|
|
const message = error instanceof Error ? error.message : "Authentication service unavailable."
|
|
return NextResponse.json(
|
|
{ error: message },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
}
|