{"version":3,"file":"_id_.mjs","names":[],"sources":["../../../../../src/astro/routes/api/media/[id].ts"],"sourcesContent":["/**\n * Single media item endpoint\n *\n * GET /_emdash/api/media/:id - Get media item\n * PUT /_emdash/api/media/:id - Update media metadata\n * DELETE /_emdash/api/media/:id - Delete media item\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { canReadMediaUsageCount, requireOwnerPerm, requirePerm } from \"#api/authorize.js\";\nimport { apiError, apiSuccess, handleError, unwrapResult } from \"#api/error.js\";\nimport { handleMediaUsageSummaries } from \"#api/handlers/media-usage.js\";\nimport { isParseError, parseBody, parseQuery } from \"#api/parse.js\";\nimport { mediaGetQuery, mediaUpdateBody } from \"#api/schemas.js\";\nimport { MediaRepository } from \"#db/repositories/media.js\";\nimport { removeUploadAttempt } from \"#media/upload-attempts.js\";\n\nexport const prerender = false;\n\n/**\n * Get media item\n */\nexport const GET: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst { id } = params;\n\n\tconst readDenied = requirePerm(user, \"media:read\");\n\tif (readDenied) return readDenied;\n\n\tif (!id) {\n\t\treturn apiError(\"INVALID_REQUEST\", \"Media ID required\", 400);\n\t}\n\n\tif (!emdash?.handleMediaGet) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\tconst query = parseQuery(new URL(request.url), mediaGetQuery);\n\tif (isParseError(query)) return query;\n\n\tconst result = await emdash.handleMediaGet(id);\n\tif (!result.success || query.includeUsage !== \"1\") return unwrapResult(result);\n\n\tconst includeCount = canReadMediaUsageCount(user);\n\tconst usageResult = await handleMediaUsageSummaries(emdash.db, [id], { includeCount });\n\tif (!usageResult.success) return unwrapResult(usageResult);\n\tconst usage = usageResult.data[id];\n\tif (!usage) return apiError(\"MEDIA_USAGE_READ_ERROR\", \"Failed to read media usage\", 500);\n\n\treturn apiSuccess({ item: { ...result.data.item, usage } });\n};\n\n/**\n * Update media metadata\n *\n * Authors can edit their own media; editors+ can edit any.\n */\nexport const PUT: APIRoute = async ({ params, request, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst { id } = params;\n\n\t// Minimum permission gate — ownership checked below\n\tconst editDenied = requirePerm(user, \"media:edit_own\");\n\tif (editDenied) return editDenied;\n\n\tif (!id) {\n\t\treturn apiError(\"INVALID_REQUEST\", \"Media ID required\", 400);\n\t}\n\n\tif (!emdash?.handleMediaGet || !emdash?.handleMediaUpdate) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\t// Fetch media item for ownership check\n\t\tconst getResult = await emdash.handleMediaGet(id);\n\t\tif (!getResult.success || !getResult.data?.item) {\n\t\t\treturn apiError(\"NOT_FOUND\", \"Media item not found\", 404);\n\t\t}\n\n\t\tconst media = getResult.data.item;\n\n\t\t// Ownership check: authors can edit own, editors+ can edit any\n\t\tconst ownerDenied = requireOwnerPerm(user, media.authorId, \"media:edit_own\", \"media:edit_any\");\n\t\tif (ownerDenied) return ownerDenied;\n\n\t\tconst body = await parseBody(request, mediaUpdateBody);\n\t\tif (isParseError(body)) return body;\n\n\t\tconst result = await emdash.handleMediaUpdate(id, {\n\t\t\talt: body.alt,\n\t\t\tcaption: body.caption,\n\t\t\twidth: body.width,\n\t\t\theight: body.height,\n\t\t});\n\n\t\treturn unwrapResult(result);\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to update media\", \"MEDIA_UPDATE_ERROR\");\n\t}\n};\n\n/**\n * Delete media item\n *\n * Authors can delete their own media; editors+ can delete any.\n */\nexport const DELETE: APIRoute = async ({ params, locals }) => {\n\tconst { emdash, user } = locals;\n\tconst { id } = params;\n\n\t// Minimum permission gate — ownership checked below\n\tconst deleteDenied = requirePerm(user, \"media:delete_own\");\n\tif (deleteDenied) return deleteDenied;\n\n\tif (!id) {\n\t\treturn apiError(\"INVALID_REQUEST\", \"Media ID required\", 400);\n\t}\n\n\tif (!emdash?.handleMediaGet || !emdash?.handleMediaDelete) {\n\t\treturn apiError(\"NOT_CONFIGURED\", \"EmDash is not initialized\", 500);\n\t}\n\n\ttry {\n\t\t// Fetch media item for ownership check and storage key\n\t\tconst getResult = await emdash.handleMediaGet(id);\n\t\tif (!getResult.success || !getResult.data?.item) {\n\t\t\treturn apiError(\"NOT_FOUND\", \"Media item not found\", 404);\n\t\t}\n\n\t\tconst media = getResult.data.item;\n\n\t\t// Ownership check: authors can delete own, editors+ can delete any\n\t\tconst ownerDenied = requireOwnerPerm(\n\t\t\tuser,\n\t\t\tmedia.authorId,\n\t\t\t\"media:delete_own\",\n\t\t\t\"media:delete_any\",\n\t\t);\n\t\tif (ownerDenied) return ownerDenied;\n\n\t\t// Delete from database — site-settings cache invalidation happens\n\t\t// in `EmDashRuntime.handleMediaDelete` so MCP/plugin paths inherit it.\n\t\tconst result = await emdash.handleMediaDelete(id);\n\t\tif (!result.success) return unwrapResult(result);\n\t\tif (\n\t\t\ttypeof result.data !== \"object\" ||\n\t\t\tresult.data === null ||\n\t\t\t!(\"storageKey\" in result.data) ||\n\t\t\ttypeof result.data.storageKey !== \"string\"\n\t\t) {\n\t\t\treturn apiError(\"MEDIA_DELETE_ERROR\", \"Failed to delete media\", 500);\n\t\t}\n\n\t\tif (emdash.storage) {\n\t\t\tconst repo = new MediaRepository(emdash.db);\n\t\t\tawait removeUploadAttempt(emdash.storage, repo, result.data.storageKey, {\n\t\t\t\tallowUntracked: true,\n\t\t\t});\n\t\t}\n\n\t\treturn apiSuccess({ deleted: true });\n\t} catch (error) {\n\t\treturn handleError(error, \"Failed to delete media\", \"MEDIA_DELETE_ERROR\");\n\t}\n};\n"],"mappings":"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"}