Use attachment-size limit for company logos

This commit is contained in:
Dotta
2026-03-16 10:13:19 -05:00
parent 4dfd862f11
commit 6eceb9b886
4 changed files with 12 additions and 18 deletions

View File

@@ -61,6 +61,8 @@ Valid image content types:
- `image/gif` - `image/gif`
- `image/svg+xml` - `image/svg+xml`
Company logo uploads use the normal Paperclip attachment size limit.
Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`. Then set the company logo by PATCHing the returned `assetId` into `logoAssetId`.
## Archive Company ## Archive Company

View File

@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import express from "express"; import express from "express";
import request from "supertest"; import request from "supertest";
import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { assetRoutes } from "../routes/assets.js"; import { assetRoutes } from "../routes/assets.js";
import type { StorageService } from "../storage/types.js"; import type { StorageService } from "../storage/types.js";
@@ -195,30 +196,30 @@ describe("POST /api/companies/:companyId/logo", () => {
expect(body).not.toContain("https://evil.example/"); expect(body).not.toContain("https://evil.example/");
}); });
it("allows a logo exactly 100 KB in size", async () => { it("allows logo uploads within the general attachment limit", async () => {
const png = createStorageService("image/png"); const png = createStorageService("image/png");
const app = createApp(png); const app = createApp(png);
createAssetMock.mockResolvedValue(createAsset()); createAssetMock.mockResolvedValue(createAsset());
const file = Buffer.alloc(100 * 1024, "a"); const file = Buffer.alloc(150 * 1024, "a");
const res = await request(app) const res = await request(app)
.post("/api/companies/company-1/logo") .post("/api/companies/company-1/logo")
.attach("file", file, "exact-limit.png"); .attach("file", file, "within-limit.png");
expect(res.status).toBe(201); expect(res.status).toBe(201);
}); });
it("rejects logo files larger than 100 KB", async () => { it("rejects logo files larger than the general attachment limit", async () => {
const app = createApp(createStorageService()); const app = createApp(createStorageService());
createAssetMock.mockResolvedValue(createAsset()); createAssetMock.mockResolvedValue(createAsset());
const file = Buffer.alloc(100 * 1024 + 1, "a"); const file = Buffer.alloc(MAX_ATTACHMENT_BYTES + 1, "a");
const res = await request(app) const res = await request(app)
.post("/api/companies/company-1/logo") .post("/api/companies/company-1/logo")
.attach("file", file, "too-large.png"); .attach("file", file, "too-large.png");
expect(res.status).toBe(422); expect(res.status).toBe(422);
expect(res.body.error).toBe("Image exceeds 102400 bytes"); expect(res.body.error).toBe(`Image exceeds ${MAX_ATTACHMENT_BYTES} bytes`);
}); });
it("rejects unsupported image types", async () => { it("rejects unsupported image types", async () => {

View File

@@ -8,7 +8,6 @@ import type { StorageService } from "../storage/types.js";
import { assetService, logActivity } from "../services/index.js"; import { assetService, logActivity } from "../services/index.js";
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js"; import { assertCompanyAccess, getActorInfo } from "./authz.js";
const MAX_COMPANY_LOGO_BYTES = 100 * 1024;
const SVG_CONTENT_TYPE = "image/svg+xml"; const SVG_CONTENT_TYPE = "image/svg+xml";
const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([ const ALLOWED_COMPANY_LOGO_CONTENT_TYPES = new Set([
"image/png", "image/png",
@@ -92,7 +91,7 @@ export function assetRoutes(db: Db, storage: StorageService) {
}); });
const companyLogoUpload = multer({ const companyLogoUpload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: { fileSize: MAX_COMPANY_LOGO_BYTES + 1, files: 1 }, limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
}); });
async function runSingleFileUpload( async function runSingleFileUpload(
@@ -157,10 +156,6 @@ export function assetRoutes(db: Db, storage: StorageService) {
res.status(422).json({ error: "Image is empty" }); res.status(422).json({ error: "Image is empty" });
return; return;
} }
if (fileBody.length > MAX_COMPANY_LOGO_BYTES) {
res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` });
return;
}
const actor = getActorInfo(req); const actor = getActorInfo(req);
const stored = await storage.putFile({ const stored = await storage.putFile({
@@ -224,7 +219,7 @@ export function assetRoutes(db: Db, storage: StorageService) {
} catch (err) { } catch (err) {
if (err instanceof multer.MulterError) { if (err instanceof multer.MulterError) {
if (err.code === "LIMIT_FILE_SIZE") { if (err.code === "LIMIT_FILE_SIZE") {
res.status(422).json({ error: `Image exceeds ${MAX_COMPANY_LOGO_BYTES} bytes` }); res.status(422).json({ error: `Image exceeds ${MAX_ATTACHMENT_BYTES} bytes` });
return; return;
} }
res.status(400).json({ error: err.message }); res.status(400).json({ error: err.message });

View File

@@ -160,10 +160,6 @@ export function CompanySettings() {
const file = event.target.files?.[0] ?? null; const file = event.target.files?.[0] ?? null;
event.currentTarget.value = ""; event.currentTarget.value = "";
if (!file) return; if (!file) return;
if (file.size > 100 * 1024) {
setLogoUploadError("Logo image must be 100 KB or smaller.");
return;
}
setLogoUploadError(null); setLogoUploadError(null);
logoUploadMutation.mutate(file); logoUploadMutation.mutate(file);
} }
@@ -276,7 +272,7 @@ export function CompanySettings() {
<div className="flex-1 space-y-3"> <div className="flex-1 space-y-3">
<Field <Field
label="Logo" label="Logo"
hint="Upload a PNG, JPEG, WEBP, GIF, or SVG logo image. Maximum size: 100 KB." hint="Upload a PNG, JPEG, WEBP, GIF, or SVG logo image."
> >
<div className="space-y-2"> <div className="space-y-2">
<input <input