Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 1x 1x 5x 5x 5x 5x 5x 2x 2x 2x 3x 1x 6x 6x 6x 6x 6x 4x 4x 4x 4x 4x 1x 3x 1x 2x 1x 1x 1x 5x 1x 5x 5x 5x 5x 5x 5x 2x 1x 4x 1x 7x 7x 7x 7x 7x 7x 4x 3x 3x 1x 2x 2x 2x 5x | // CRM Documents Routes - File upload/download/delete
import { Hono } from "hono";
import type { Dal } from "../../../dal";
import type { Services } from "../../../services";
import { success, handleError } from "../../../lib/response";
import { parseRequiredId } from "../../../lib/utils";
import { requireProAccess } from "../../../middleware";
import { ValidationError } from "../../../lib/errors";
type Env = {
Bindings: CloudflareBindings;
Variables: {
user: { id: string; name: string; email: string } | null;
session: unknown;
dal: Dal;
services: Services;
proId: string;
proRole: string;
};
};
const documents = new Hono<Env>();
// List documents for a lead
documents.get(
"/:proId/crm/leads/:leadId/documents",
requireProAccess,
async (c) => {
try {
const services = c.get("services");
const proId = c.get("proId");
const leadId = parseRequiredId(c.req.param("leadId"), "lead");
await services.leads.verifyProOwnership(leadId, proId);
const documentType = c.req.query("type");
const docs = await services.documents.list(leadId, documentType);
return success(c, docs);
} catch (err) {
return handleError(c, err);
}
},
);
// Upload document (multipart form)
documents.post(
"/:proId/crm/leads/:leadId/documents",
requireProAccess,
async (c) => {
try {
const services = c.get("services");
const proId = c.get("proId");
const leadId = parseRequiredId(c.req.param("leadId"), "lead");
await services.leads.verifyProOwnership(leadId, proId);
const formData = await c.req.formData();
const file = formData.get("file") as File | null;
const name = formData.get("name") as string | null;
const documentType = formData.get("documentType") as string | null;
if (!file) {
throw new ValidationError("No file provided");
}
if (!name) {
throw new ValidationError("Document name is required");
}
if (!documentType) {
throw new ValidationError("Document type is required");
}
const doc = await services.documents.upload(
leadId,
proId,
file,
{ name, documentType },
c.env.R2,
);
return success(c, doc, 201);
} catch (err) {
return handleError(c, err);
}
},
);
// Soft delete document
documents.delete(
"/:proId/crm/leads/:leadId/documents/:documentId",
requireProAccess,
async (c) => {
try {
const services = c.get("services");
const proId = c.get("proId");
const leadId = parseRequiredId(c.req.param("leadId"), "lead");
const documentId = parseRequiredId(c.req.param("documentId"), "document");
await services.leads.verifyProOwnership(leadId, proId);
await services.documents.delete(documentId, leadId);
return success(c, { deleted: true });
} catch (err) {
return handleError(c, err);
}
},
);
// Download document (proxy from R2)
documents.get(
"/:proId/crm/leads/:leadId/documents/:documentId/download",
requireProAccess,
async (c) => {
try {
const services = c.get("services");
const proId = c.get("proId");
const leadId = parseRequiredId(c.req.param("leadId"), "lead");
const documentId = parseRequiredId(c.req.param("documentId"), "document");
await services.leads.verifyProOwnership(leadId, proId);
const { fileKey, filename, mimeType } =
await services.documents.getDownloadUrl(documentId, leadId);
// Fetch from R2 and proxy to client
const object = await c.env.R2.get(fileKey);
if (!object) {
throw new ValidationError("File not found in storage");
}
// Sanitize filename to prevent header injection
const safeFilename = filename
.replace(/[\r\n]/g, "") // Strip CR/LF
.replace(/["\\/]/g, "_"); // Replace quotes/slashes
const encodedFilename = encodeURIComponent(safeFilename);
return new Response(object.body, {
headers: {
"Content-Type": mimeType,
"Content-Disposition": `attachment; filename="${safeFilename}"; filename*=UTF-8''${encodedFilename}`,
"Content-Security-Policy": "default-src 'none'",
"X-Content-Type-Options": "nosniff",
},
});
} catch (err) {
return handleError(c, err);
}
},
);
export default documents;
|