Allow bearer auth for admin API

This commit is contained in:
lucast committed 2026-10-05 02:01:40 +02:00
1 parent 142e34babe
commit fb99058492
14 files changed
+47 -28

No files matched your search

+26 -14
View File
@@ -79,19 +79,27 @@ export function getSessionTtl(event?: H3Event) {
return parseDuration(tokenExpire) || 60 * 60 * 1000
}
export async function findSession(event: H3Event) {
export async function findSession(event: H3Event, allowBearer = false) {
const Session = getSessionModel(getSessionTtl(event)) as mongoose.Model<any>
const token = getCookie(event, SESSION_COOKIE)
if (!token) return null
const cookieToken = getCookie(event, SESSION_COOKIE)
const authorization = allowBearer ? getRequestHeader(event, "authorization") : undefined
const bearerToken = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]
const credentials = [
...(bearerToken ? [{ token: bearerToken, source: "bearer" as const }] : []),
...(cookieToken ? [{ token: cookieToken, source: "cookie" as const }] : []),
]
const session = await Session.findOne({ token })
.populate("userId", "+verified")
.exec()
const user = session?.userId as unknown as
| { _id: unknown; verified?: boolean; role?: string }
| undefined
for (const credential of credentials) {
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) return { session, user, source: credential.source }
}
return session && user ? { session, user } : null
return null
}
export async function findAuthenticatedSession(event: H3Event, adminOnly = false) {
@@ -100,9 +108,13 @@ export async function findAuthenticatedSession(event: H3Event, adminOnly = false
return auth
}
export async function requireAuthenticatedUser(event: H3Event, adminOnly = false) {
const auth = await findAuthenticatedSession(event, adminOnly)
if (!auth) {
export async function requireAuthenticatedUser(
event: H3Event,
adminOnly = false,
options: { allowBearer?: boolean; skipOriginCheck?: boolean } = {},
) {
const auth = await findSession(event, options.allowBearer)
if (!auth?.user.verified || (adminOnly && auth.user.role !== "admin")) {
throw createError({
statusCode: 401,
statusMessage: "Unauthorized",
@@ -112,7 +124,7 @@ export async function requireAuthenticatedUser(event: H3Event, adminOnly = false
const method = event.node.req.method?.toUpperCase() || "GET"
const isUnsafeMethod = !["GET", "HEAD", "OPTIONS"].includes(method)
if (isUnsafeMethod) assertCookieRequestOrigin(event)
if (isUnsafeMethod && !options.skipOriginCheck && auth.source === "cookie") assertCookieRequestOrigin(event)
return auth.user
}
+9 -2
View File
@@ -124,7 +124,7 @@ function operationSpec(operation: ApiOperation) {
),
}
: {}),
...((operation.method === "post" && operation.path.startsWith("/auth/")) || ((operation.auth || operation.admin) && operation.method !== "get")
...((operation.method === "post" && operation.path.startsWith("/auth/")) || ((operation.auth || operation.admin) && operation.method !== "get" && !operation.path.startsWith("/admin/"))
? { 403: jsonResponse("Invalid request origin", ref("Error")) }
: {}),
...(operation.path === "/user/tracks" || (operation.path.startsWith("/admin/") && (operation.method === "patch" || operation.method === "delete"))
@@ -140,7 +140,13 @@ function operationSpec(operation: ApiOperation) {
return {
tags: [operation.tag],
summary: operation.summary,
...(operation.auth || operation.admin ? { security: [{ SessionCookie: [] }] } : {}),
...(operation.auth || operation.admin
? {
security: operation.admin
? [{ SessionCookie: [] }, { BearerSession: [] }]
: [{ SessionCookie: [] }],
}
: {}),
...(parameters.length ? { parameters } : {}),
...(operation.body
? {
@@ -175,6 +181,7 @@ export const openApiDocument = {
components: {
securitySchemes: {
SessionCookie: { type: "apiKey", in: "cookie", name: "musicSession" },
BearerSession: { type: "http", scheme: "bearer", description: "Session token; supported by admin endpoints." },
},
schemas: {
Error: { type: "object", required: ["message"], properties: { message: { type: "string" } } },