Rocky_Mountain_Vending/lib/server/admin-auth.ts

160 lines
4.1 KiB
TypeScript

import { createHash, randomBytes } from "node:crypto"
import { cookies } from "next/headers"
import { fetchMutation, fetchQuery } from "convex/nextjs"
import { NextResponse } from "next/server"
import { api } from "@/convex/_generated/api"
import { hasConvexUrl } from "@/lib/convex-config"
export const ADMIN_SESSION_COOKIE = "rmv_admin_session"
const ADMIN_SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 7
function getProvidedToken(request: Request) {
const authHeader = request.headers.get("authorization") || ""
const bearerToken = authHeader.startsWith("Bearer ")
? authHeader.slice("Bearer ".length).trim()
: ""
return request.headers.get("x-admin-token") || bearerToken
}
export function requireAdminToken(request: Request) {
const configuredToken = process.env.ADMIN_API_TOKEN
if (!configuredToken) {
return NextResponse.json(
{ error: "Admin API is disabled." },
{ status: 503 }
)
}
const providedToken = getProvidedToken(request)
if (providedToken !== configuredToken) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
return null
}
export function isAdminUiEnabled() {
return process.env.ADMIN_UI_ENABLED === "true"
}
export function getConfiguredAdminEmail() {
return String(process.env.ADMIN_EMAIL || "")
.trim()
.toLowerCase()
}
function getConfiguredAdminPassword() {
return String(process.env.ADMIN_PASSWORD || "")
}
function hashAdminSessionToken(token: string) {
return createHash("sha256").update(token).digest("hex")
}
function readCookieFromHeader(cookieHeader: string, name: string) {
const cookies = cookieHeader.split(";")
for (const entry of cookies) {
const [cookieName, ...rest] = entry.trim().split("=")
if (cookieName === name) {
return rest.join("=")
}
}
return ""
}
export function isAdminCredentialLoginConfigured() {
return Boolean(
isAdminUiEnabled() &&
hasConvexUrl() &&
getConfiguredAdminEmail() &&
getConfiguredAdminPassword()
)
}
export function isAdminCredentialMatch(email: string, password: string) {
return (
email.trim().toLowerCase() === getConfiguredAdminEmail() &&
password === getConfiguredAdminPassword()
)
}
export async function createAdminSession(email: string) {
if (!hasConvexUrl()) {
throw new Error("Convex is not configured for admin sessions.")
}
const normalizedEmail = email.trim().toLowerCase()
const rawToken = randomBytes(32).toString("hex")
const tokenHash = hashAdminSessionToken(rawToken)
const expiresAt = Date.now() + ADMIN_SESSION_TTL_MS
await fetchMutation(api.admin.ensureAdminUser, {
email: normalizedEmail,
name: normalizedEmail.split("@")[0],
})
await fetchMutation(api.admin.createSession, {
email: normalizedEmail,
tokenHash,
expiresAt,
})
return {
token: rawToken,
expiresAt,
}
}
export async function destroyAdminSession(rawToken?: string | null) {
if (!rawToken || !hasConvexUrl()) {
return
}
try {
await fetchMutation(api.admin.destroySession, {
tokenHash: hashAdminSessionToken(rawToken),
})
} catch (error) {
console.error("Failed to destroy admin session:", error)
}
}
export async function validateAdminSession(rawToken?: string | null) {
if (!rawToken || !hasConvexUrl()) {
return null
}
try {
return await fetchQuery(api.admin.validateSession, {
tokenHash: hashAdminSessionToken(rawToken),
})
} catch (error) {
console.error("Failed to validate admin session:", error)
return null
}
}
export async function requireAdminSession(request: Request) {
const rawToken = readCookieFromHeader(
request.headers.get("cookie") || "",
ADMIN_SESSION_COOKIE
)
const session = await validateAdminSession(rawToken || null)
if (!session?.user) {
return null
}
return session.user
}
export async function getAdminUserFromCookies() {
if (!isAdminUiEnabled()) {
return null
}
const cookieStore = await cookies()
const rawToken = cookieStore.get(ADMIN_SESSION_COOKIE)?.value
const session = await validateAdminSession(rawToken)
return session?.user || null
}