import type { H3Event } from "h3" import { createError, defineEventHandler, getCookie, getRequestHeader, getRequestURL, setResponseStatus, type EventHandler, } from "h3" import parseDuration from "parse-duration" import mongoose from "mongoose" import { getSessionModel } from "../models/session" import { getBackendRuntimeConfig } from "./backend-config" export const SESSION_COOKIE = "musicSession" export const REFRESH_COOKIE = "refreshToken" const INVALID_REQUEST_ORIGIN = "Invalid request origin" /** * Keep the legacy API's direct authentication response body while leaving all * unrelated H3 errors to Nitro's normal error handler. */ export function defineAuthenticatedEventHandler(handler: EventHandler) { return defineEventHandler(async (event) => { try { return await handler(event) } catch (error) { const statusCode = (error as { statusCode?: number } | undefined)?.statusCode const statusMessage = (error as { statusMessage?: string } | undefined)?.statusMessage const message = (error as { data?: { message?: string } } | undefined)?.data?.message if (statusCode === 401 && statusMessage === "Unauthorized" && message === "Unauthorized") { setResponseStatus(event, 401) return { message: "Unauthorized" } } if (statusCode === 403 && statusMessage === "Forbidden" && message === INVALID_REQUEST_ORIGIN) { setResponseStatus(event, 403) return { message: INVALID_REQUEST_ORIGIN } } throw error } }) } export function assertCookieRequestOrigin(event: H3Event) { const originHeader = getRequestHeader(event, "origin") const refererHeader = getRequestHeader(event, "referer") let requestOrigin: string | undefined let suppliedOrigin: string | undefined try { // H3 uses Host for the request host. A trusted ingress may provide // x-forwarded-proto; it must overwrite that header before this app. requestOrigin = getRequestURL(event, { xForwardedProto: true }).origin if (originHeader !== undefined) { const parsedOrigin = new URL(originHeader) if (parsedOrigin.origin === originHeader) suppliedOrigin = parsedOrigin.origin } else if (refererHeader !== undefined) { suppliedOrigin = new URL(refererHeader).origin } } catch { // Invalid or absent browser origin proof is rejected below. } if (!suppliedOrigin || !requestOrigin || suppliedOrigin !== requestOrigin) { throw createError({ statusCode: 403, statusMessage: "Forbidden", data: { message: INVALID_REQUEST_ORIGIN }, }) } } export function getSessionTtl(event?: H3Event) { const { tokenExpire } = getBackendRuntimeConfig(event) return parseDuration(tokenExpire) || 60 * 60 * 1000 } function getSessionTokens(event: H3Event) { const tokens: { token: string; source: "cookie" | "bearer" }[] = [] const cookieToken = getCookie(event, SESSION_COOKIE) const authorization = event.node.req.headers.authorization const bearerToken = authorization?.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : undefined if (cookieToken) tokens.push({ token: cookieToken, source: "cookie" }) if (bearerToken && bearerToken !== cookieToken) { tokens.push({ token: bearerToken, source: "bearer" }) } return tokens } export async function findSession(event: H3Event) { const Session = getSessionModel(getSessionTtl(event)) as mongoose.Model for (const credential of getSessionTokens(event)) { const session = await Session.findOne({ token: credential.token }) .populate("userId", "+verified") .exec() const user = session?.userId as unknown as | { _id: unknown; verified?: boolean; role?: string } | undefined if (!session || !user) continue const bearerToken = event.node.req.headers.authorization?.startsWith("Bearer ") ? event.node.req.headers.authorization.slice("Bearer ".length) : undefined // If the cookie and bearer header carry the same valid token, the request // is independently authenticated by the explicit bearer credential. const source = bearerToken === credential.token ? "bearer" : credential.source return { session, user, source } } return null } export async function findAuthenticatedSession(event: H3Event, adminOnly = false) { const auth = await findSession(event) if (!auth?.user.verified || (adminOnly && auth.user.role !== "admin")) return null return auth } export async function requireAuthenticatedUser(event: H3Event, adminOnly = false) { const auth = await findAuthenticatedSession(event, adminOnly) if (!auth) { throw createError({ statusCode: 401, statusMessage: "Unauthorized", data: { message: "Unauthorized" }, }) } const method = event.node.req.method?.toUpperCase() || "GET" const isUnsafeMethod = !["GET", "HEAD", "OPTIONS"].includes(method) if (isUnsafeMethod && auth.source === "cookie") { assertCookieRequestOrigin(event) } return auth.user }