Use attachment-size limit for company logos
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user