diff --git a/server/api/admin/album.delete.ts b/server/api/admin/album.delete.ts index 11ddbe8..6814d76 100644 --- a/server/api/admin/album.delete.ts +++ b/server/api/admin/album.delete.ts @@ -6,7 +6,7 @@ import { parseMongoId, validationResponse } from "../../utils/api-validation" import { tryDeleteObject, tryDeleteObjectPrefix } from "../../utils/object-storage" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const query = getQuery(event) const parsedId = parseMongoId(query.id) if ("error" in parsedId) return validationResponse(event, parsedId.error) diff --git a/server/api/admin/album.patch.ts b/server/api/admin/album.patch.ts index 7bbacab..e0a1381 100644 --- a/server/api/admin/album.patch.ts +++ b/server/api/admin/album.patch.ts @@ -7,7 +7,7 @@ import { parseMongoId, validationResponse } from "../../utils/api-validation" import { Album } from "../../models/album" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parsedId = parseMongoId(getQuery(event).id) if ("error" in parsedId) return validationResponse(event, parsedId.error) const parts = await readAdminForm(event) diff --git a/server/api/admin/album.post.ts b/server/api/admin/album.post.ts index 2a253b2..d201ad3 100644 --- a/server/api/admin/album.post.ts +++ b/server/api/admin/album.post.ts @@ -5,7 +5,7 @@ import { Album } from "../../models/album" import mongoose from "mongoose" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parts = await readAdminForm(event) const name = adminFormText(parts, "name") const artistInput = adminFormTexts(parts, "artists") diff --git a/server/api/admin/artist.delete.ts b/server/api/admin/artist.delete.ts index 5472933..541e029 100644 --- a/server/api/admin/artist.delete.ts +++ b/server/api/admin/artist.delete.ts @@ -11,7 +11,7 @@ async function findDependents(model: typeof Track | typeof Album, field: string, } export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const query = getQuery(event) const parsedId = parseMongoId(query.id) if ("error" in parsedId) return validationResponse(event, parsedId.error) diff --git a/server/api/admin/artist.patch.ts b/server/api/admin/artist.patch.ts index d5c9b6c..ffb1441 100644 --- a/server/api/admin/artist.patch.ts +++ b/server/api/admin/artist.patch.ts @@ -7,7 +7,7 @@ import { parseMongoId, validationResponse } from "../../utils/api-validation" import { Artist } from "../../models/artist" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parsedId = parseMongoId(getQuery(event).id) if ("error" in parsedId) return validationResponse(event, parsedId.error) const parts = await readAdminForm(event) diff --git a/server/api/admin/artist.post.ts b/server/api/admin/artist.post.ts index 1a463bf..b7bfd61 100644 --- a/server/api/admin/artist.post.ts +++ b/server/api/admin/artist.post.ts @@ -4,7 +4,7 @@ import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../.. import { Artist } from "../../models/artist" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parts = await readAdminForm(event) const name = adminFormText(parts, "name") const file = adminFormFile(parts, "file") diff --git a/server/api/admin/genre.delete.ts b/server/api/admin/genre.delete.ts index 921fa15..6f5c35f 100644 --- a/server/api/admin/genre.delete.ts +++ b/server/api/admin/genre.delete.ts @@ -5,7 +5,7 @@ import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../.. import { parseMongoId } from "../../utils/api-validation" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const query = getQuery(event) const parsedId = parseMongoId(query.id) diff --git a/server/api/admin/genre.patch.ts b/server/api/admin/genre.patch.ts index 5075ba0..923c588 100644 --- a/server/api/admin/genre.patch.ts +++ b/server/api/admin/genre.patch.ts @@ -4,7 +4,7 @@ import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../.. import { parseMongoId } from "../../utils/api-validation" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parsedId = parseMongoId(getQuery(event).id) if ("error" in parsedId) { diff --git a/server/api/admin/genre.post.ts b/server/api/admin/genre.post.ts index e519f38..53060e2 100644 --- a/server/api/admin/genre.post.ts +++ b/server/api/admin/genre.post.ts @@ -3,7 +3,7 @@ import { Genre } from "../../models/genre" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const body = await readBody(event) if (!body || typeof body !== "object" || Array.isArray(body)) { diff --git a/server/api/admin/track.delete.ts b/server/api/admin/track.delete.ts index 8bcf357..fac050c 100644 --- a/server/api/admin/track.delete.ts +++ b/server/api/admin/track.delete.ts @@ -5,7 +5,7 @@ import { parseMongoId, validationResponse } from "../../utils/api-validation" import { tryDeleteObjectPrefix } from "../../utils/object-storage" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parsedId = parseMongoId(getQuery(event).id) if ("error" in parsedId) return validationResponse(event, parsedId.error) diff --git a/server/api/admin/track.patch.ts b/server/api/admin/track.patch.ts index fbc1a92..a91c3ad 100644 --- a/server/api/admin/track.patch.ts +++ b/server/api/admin/track.patch.ts @@ -8,7 +8,7 @@ import { parseMongoId, validationResponse } from "../../utils/api-validation" import { Track } from "../../models/track" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parsedId = parseMongoId(getQuery(event).id) if ("error" in parsedId) return validationResponse(event, parsedId.error) const parts = await readAdminForm(event) diff --git a/server/api/admin/track.post.ts b/server/api/admin/track.post.ts index 6e83809..13fcda3 100644 --- a/server/api/admin/track.post.ts +++ b/server/api/admin/track.post.ts @@ -6,7 +6,7 @@ import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../.. import { Track } from "../../models/track" export default defineAuthenticatedEventHandler(async (event) => { - await requireAuthenticatedUser(event, true) + await requireAuthenticatedUser(event, true, { allowBearer: true, skipOriginCheck: true }) const parts = await readAdminForm(event) const name = adminFormText(parts, "name") const album = adminFormText(parts, "album") diff --git a/server/utils/auth.ts b/server/utils/auth.ts index 09e7443..16477d3 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -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 - 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 } diff --git a/server/utils/openapi.ts b/server/utils/openapi.ts index 3dc45aa..0127882 100644 --- a/server/utils/openapi.ts +++ b/server/utils/openapi.ts @@ -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" } } },