160 lines
4.1 KiB
TypeScript
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
|
|
}
|