Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { Router, type Request, type Response } from "express";
|
|
|
|
|
import multer from "multer";
|
2026-03-06 17:18:43 -05:00
|
|
|
import createDOMPurify from "dompurify";
|
|
|
|
|
import { JSDOM } from "jsdom";
|
2026-03-03 08:45:26 -06:00
|
|
|
import type { Db } from "@paperclipai/db";
|
|
|
|
|
import { createAssetImageMetadataSchema } from "@paperclipai/shared";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import type { StorageService } from "../storage/types.js";
|
|
|
|
|
import { assetService, logActivity } from "../services/index.js";
|
2026-03-10 19:40:22 +05:30
|
|
|
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
2026-03-06 17:18:43 -05:00
|
|
|
const SVG_CONTENT_TYPE = "image/svg+xml";
|
2026-03-16 10:05:14 -05:00
|
|
|
const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([
|
|
|
|
|
"image/png",
|
|
|
|
|
"image/jpeg",
|
|
|
|
|
"image/jpg",
|
|
|
|
|
"image/webp",
|
|
|
|
|
"image/gif",
|
|
|
|
|
SVG_CONTENT_TYPE,
|
|
|
|
|
]);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
2026-03-06 17:18:43 -05:00
|
|
|
function sanitizeSvgBuffer(input: Buffer): Buffer | null {
|
|
|
|
|
const raw = input.toString("utf8").trim();
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
|
|
|
|
|
const baseDom = new JSDOM("");
|
|
|
|
|
const domPurify = createDOMPurify(
|
|
|
|
|
baseDom.window as unknown as Parameters<typeof createDOMPurify>[0],
|
|
|
|
|
);
|
|
|
|
|
domPurify.addHook("uponSanitizeAttribute", (_node, data) => {
|
|
|
|
|
const attrName = data.attrName.toLowerCase();
|
|
|
|
|
const attrValue = (data.attrValue ?? "").trim();
|
|
|
|
|
|
|
|
|
|
if (attrName.startsWith("on")) {
|
|
|
|
|
data.keepAttr = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) {
|
|
|
|
|
data.keepAttr = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let parsedDom: JSDOM | null = null;
|
|
|
|
|
try {
|
|
|
|
|
const sanitized = domPurify.sanitize(raw, {
|
|
|
|
|
USE_PROFILES: { svg: true, svgFilters: true, html: false },
|
|
|
|
|
FORBID_TAGS: ["script", "foreignObject"],
|
|
|
|
|
FORBID_CONTENTS: ["script", "foreignObject"],
|
|
|
|
|
RETURN_TRUSTED_TYPE: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
parsedDom = new JSDOM(sanitized, { contentType: SVG_CONTENT_TYPE });
|
|
|
|
|
const document = parsedDom.window.document;
|
|
|
|
|
const root = document.documentElement;
|
|
|
|
|
if (!root || root.tagName.toLowerCase() !== "svg") return null;
|
|
|
|
|
|
|
|
|
|
for (const el of Array.from(root.querySelectorAll("script, foreignObject"))) {
|
|
|
|
|
el.remove();
|
|
|
|
|
}
|
|
|
|
|
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
|
|
|
for (const attr of Array.from(el.attributes)) {
|
|
|
|
|
const attrName = attr.name.toLowerCase();
|
|
|
|
|
const attrValue = attr.value.trim();
|
|
|
|
|
if (attrName.startsWith("on")) {
|
|
|
|
|
el.removeAttribute(attr.name);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ((attrName === "href" || attrName === "xlink:href") && attrValue && !attrValue.startsWith("#")) {
|
|
|
|
|
el.removeAttribute(attr.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const output = root.outerHTML.trim();
|
|
|
|
|
if (!output || !/^<svg[\s>]/i.test(output)) return null;
|
|
|
|
|
return Buffer.from(output, "utf8");
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
} finally {
|
|
|
|
|
parsedDom?.window.close();
|
|
|
|
|
baseDom.window.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
export function assetRoutes(db: Db, storage: StorageService) {
|
|
|
|
|
const router = Router();
|
|
|
|
|
const svc = assetService(db);
|
2026-03-16 10:05:14 -05:00
|
|
|
const assetUpload = multer({
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
storage: multer.memoryStorage(),
|
2026-03-10 19:40:22 +05:30
|
|
|
limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
});
|
2026-03-16 10:05:14 -05:00
|
|
|
const companyLogoUpload = multer({
|
|
|
|
|
storage: multer.memoryStorage(),
|
2026-03-16 10:13:19 -05:00
|
|
|
limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
|
2026-03-16 10:05:14 -05:00
|
|
|
});
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
2026-03-16 10:05:14 -05:00
|
|
|
async function runSingleFileUpload(
|
|
|
|
|
upload: ReturnType<typeof multer>,
|
|
|
|
|
req: Request,
|
|
|
|
|
res: Response,
|
|
|
|
|
) {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
upload.single("file")(req, res, (err: unknown) => {
|
|
|
|
|
if (err) reject(err);
|
|
|
|
|
else resolve();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.post("/companies/:companyId/assets/images", async (req, res) => {
|
|
|
|
|
const companyId = req.params.companyId as string;
|
|
|
|
|
assertCompanyAccess(req, companyId);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-16 10:05:14 -05:00
|
|
|
await runSingleFileUpload(assetUpload, req, res);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof multer.MulterError) {
|
|
|
|
|
if (err.code === "LIMIT_FILE_SIZE") {
|
2026-03-10 20:01:08 +05:30
|
|
|
res.status(422).json({ error: `File exceeds ${MAX_ATTACHMENT_BYTES} bytes` });
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
res.status(400).json({ error: err.message });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = (req as Request & { file?: { mimetype: string; buffer: Buffer; originalname: string } }).file;
|
|
|
|
|
if (!file) {
|
|
|
|
|
res.status(400).json({ error: "Missing file field 'file'" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 09:25:39 -05:00
|
|
|
const parsedMeta = createAssetImageMetadataSchema.safeParse(req.body ?? {});
|
|
|
|
|
if (!parsedMeta.success) {
|
|
|
|
|
res.status(400).json({ error: "Invalid image metadata", details: parsedMeta.error.issues });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const namespaceSuffix = parsedMeta.data.namespace ?? "general";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const contentType = (file.mimetype || "").toLowerCase();
|
2026-03-16 08:47:05 -05:00
|
|
|
if (contentType !== SVG_CONTENT_TYPE && !isAllowedContentType(contentType)) {
|
2026-03-10 19:54:42 +05:30
|
|
|
res.status(422).json({ error: `Unsupported file type: ${contentType || "unknown"}` });
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
return;
|
|
|
|
|
}
|
2026-03-06 17:18:43 -05:00
|
|
|
let fileBody = file.buffer;
|
|
|
|
|
if (contentType === SVG_CONTENT_TYPE) {
|
|
|
|
|
const sanitized = sanitizeSvgBuffer(file.buffer);
|
|
|
|
|
if (!sanitized || sanitized.length <= 0) {
|
|
|
|
|
res.status(422).json({ error: "SVG could not be sanitized" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fileBody = sanitized;
|
|
|
|
|
}
|
|
|
|
|
if (fileBody.length <= 0) {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
res.status(422).json({ error: "Image is empty" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-06 16:39:35 -05:00
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const actor = getActorInfo(req);
|
|
|
|
|
const stored = await storage.putFile({
|
|
|
|
|
companyId,
|
|
|
|
|
namespace: `assets/${namespaceSuffix}`,
|
|
|
|
|
originalFilename: file.originalname || null,
|
|
|
|
|
contentType,
|
2026-03-06 17:18:43 -05:00
|
|
|
body: fileBody,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const asset = await svc.create(companyId, {
|
|
|
|
|
provider: stored.provider,
|
|
|
|
|
objectKey: stored.objectKey,
|
|
|
|
|
contentType: stored.contentType,
|
|
|
|
|
byteSize: stored.byteSize,
|
|
|
|
|
sha256: stored.sha256,
|
|
|
|
|
originalFilename: stored.originalFilename,
|
|
|
|
|
createdByAgentId: actor.agentId,
|
|
|
|
|
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logActivity(db, {
|
|
|
|
|
companyId,
|
|
|
|
|
actorType: actor.actorType,
|
|
|
|
|
actorId: actor.actorId,
|
|
|
|
|
agentId: actor.agentId,
|
|
|
|
|
runId: actor.runId,
|
|
|
|
|
action: "asset.created",
|
|
|
|
|
entityType: "asset",
|
|
|
|
|
entityId: asset.id,
|
|
|
|
|
details: {
|
|
|
|
|
originalFilename: asset.originalFilename,
|
|
|
|
|
contentType: asset.contentType,
|
|
|
|
|
byteSize: asset.byteSize,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
assetId: asset.id,
|
|
|
|
|
companyId: asset.companyId,
|
|
|
|
|
provider: asset.provider,
|
|
|
|
|
objectKey: asset.objectKey,
|
|
|
|
|
contentType: asset.contentType,
|
|
|
|
|
byteSize: asset.byteSize,
|
|
|
|
|
sha256: asset.sha256,
|
|
|
|
|
originalFilename: asset.originalFilename,
|
|
|
|
|
createdByAgentId: asset.createdByAgentId,
|
|
|
|
|
createdByUserId: asset.createdByUserId,
|
|
|
|
|
createdAt: asset.createdAt,
|
|
|
|
|
updatedAt: asset.updatedAt,
|
|
|
|
|
contentPath: `/api/assets/${asset.id}/content`,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-16 10:05:14 -05:00
|
|
|
router.post("/companies/:companyId/logo", async (req, res) => {
|
|
|
|
|
const companyId = req.params.companyId as string;
|
|
|
|
|
assertCompanyAccess(req, companyId);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await runSingleFileUpload(companyLogoUpload, req, res);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err instanceof multer.MulterError) {
|
|
|
|
|
if (err.code === "LIMIT_FILE_SIZE") {
|
2026-03-16 10:13:19 -05:00
|
|
|
res.status(422).json({ error: `Image exceeds ${MAX_ATTACHMENT_BYTES} bytes` });
|
2026-03-16 10:05:14 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
res.status(400).json({ error: err.message });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = (req as Request & { file?: { mimetype: string; buffer: Buffer; originalname: string } }).file;
|
|
|
|
|
if (!file) {
|
|
|
|
|
res.status(400).json({ error: "Missing file field 'file'" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contentType = (file.mimetype || "").toLowerCase();
|
|
|
|
|
if (!ALLOWED_COMPANY_LOGO_CONTENT_TYPES.has(contentType)) {
|
|
|
|
|
res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fileBody = file.buffer;
|
|
|
|
|
if (contentType === SVG_CONTENT_TYPE) {
|
|
|
|
|
const sanitized = sanitizeSvgBuffer(file.buffer);
|
|
|
|
|
if (!sanitized || sanitized.length <= 0) {
|
|
|
|
|
res.status(422).json({ error: "SVG could not be sanitized" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fileBody = sanitized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fileBody.length <= 0) {
|
|
|
|
|
res.status(422).json({ error: "Image is empty" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actor = getActorInfo(req);
|
|
|
|
|
const stored = await storage.putFile({
|
|
|
|
|
companyId,
|
|
|
|
|
namespace: "assets/companies",
|
|
|
|
|
originalFilename: file.originalname || null,
|
|
|
|
|
contentType,
|
|
|
|
|
body: fileBody,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const asset = await svc.create(companyId, {
|
|
|
|
|
provider: stored.provider,
|
|
|
|
|
objectKey: stored.objectKey,
|
|
|
|
|
contentType: stored.contentType,
|
|
|
|
|
byteSize: stored.byteSize,
|
|
|
|
|
sha256: stored.sha256,
|
|
|
|
|
originalFilename: stored.originalFilename,
|
|
|
|
|
createdByAgentId: actor.agentId,
|
|
|
|
|
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await logActivity(db, {
|
|
|
|
|
companyId,
|
|
|
|
|
actorType: actor.actorType,
|
|
|
|
|
actorId: actor.actorId,
|
|
|
|
|
agentId: actor.agentId,
|
|
|
|
|
runId: actor.runId,
|
|
|
|
|
action: "asset.created",
|
|
|
|
|
entityType: "asset",
|
|
|
|
|
entityId: asset.id,
|
|
|
|
|
details: {
|
|
|
|
|
originalFilename: asset.originalFilename,
|
|
|
|
|
contentType: asset.contentType,
|
|
|
|
|
byteSize: asset.byteSize,
|
|
|
|
|
namespace: "assets/companies",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
assetId: asset.id,
|
|
|
|
|
companyId: asset.companyId,
|
|
|
|
|
provider: asset.provider,
|
|
|
|
|
objectKey: asset.objectKey,
|
|
|
|
|
contentType: asset.contentType,
|
|
|
|
|
byteSize: asset.byteSize,
|
|
|
|
|
sha256: asset.sha256,
|
|
|
|
|
originalFilename: asset.originalFilename,
|
|
|
|
|
createdByAgentId: asset.createdByAgentId,
|
|
|
|
|
createdByUserId: asset.createdByUserId,
|
|
|
|
|
createdAt: asset.createdAt,
|
|
|
|
|
updatedAt: asset.updatedAt,
|
|
|
|
|
contentPath: `/api/assets/${asset.id}/content`,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
router.get("/assets/:assetId/content", async (req, res, next) => {
|
|
|
|
|
const assetId = req.params.assetId as string;
|
|
|
|
|
const asset = await svc.getById(assetId);
|
|
|
|
|
if (!asset) {
|
|
|
|
|
res.status(404).json({ error: "Asset not found" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
assertCompanyAccess(req, asset.companyId);
|
|
|
|
|
|
|
|
|
|
const object = await storage.getObject(asset.companyId, asset.objectKey);
|
2026-03-06 17:18:43 -05:00
|
|
|
const responseContentType = asset.contentType || object.contentType || "application/octet-stream";
|
|
|
|
|
res.setHeader("Content-Type", responseContentType);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
res.setHeader("Content-Length", String(asset.byteSize || object.contentLength || 0));
|
|
|
|
|
res.setHeader("Cache-Control", "private, max-age=60");
|
2026-03-06 17:18:43 -05:00
|
|
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
|
|
|
if (responseContentType === SVG_CONTENT_TYPE) {
|
|
|
|
|
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'");
|
|
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const filename = asset.originalFilename ?? "asset";
|
|
|
|
|
res.setHeader("Content-Disposition", `inline; filename=\"${filename.replaceAll("\"", "")}\"`);
|
|
|
|
|
|
|
|
|
|
object.stream.on("error", (err) => {
|
|
|
|
|
next(err);
|
|
|
|
|
});
|
|
|
|
|
object.stream.pipe(res);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return router;
|
|
|
|
|
}
|