mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #44330, Resolves #44331 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests. (I'd defer integration tests to a separate PR since this one is pretty large already.) - [x] QA'd all new/changed functionality manually. I've tested this on both the setup flow and the organization settings page. I haven't had the time to test this on other places where we render the logo (macOS setup experience / MDM migration dialog). https://github.com/user-attachments/assets/95d4eae5-3da6-40f4-98a1-8575b97d96b3 ## New Fleet configuration settings - [x] Setting(s) is/are explicitly excluded from GitOps. Will handle GitOps in a separate PR. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Organizations can upload custom logos for light and dark modes. * Registration and Org Settings support logo file upload, preview, per-mode replace/delete, and validation (size & image formats). * Activity feed records logo changes/deletions; site nav displays uploaded logos per theme. * File uploader/preview adds a Fleet logo graphic option and improved logo validation. * Config/GitOps outputs now include separate dark/light logo fields. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
export const ORG_LOGO_ACCEPT = ".png,.jpg,.jpeg,.webp";
|
|
export const ORG_LOGO_MAX_SIZE_BYTES = 100 * 1024; // 100 KB
|
|
export const ORG_LOGO_HELP_TEXT =
|
|
"Personalize Fleet with your brand. For best results, use a square image at least 150px wide.";
|
|
export const ORG_LOGO_ALLOWED_TYPES = ["png", "jpeg", "webp"] as const;
|
|
|
|
export type ImageFileType = typeof ORG_LOGO_ALLOWED_TYPES[number];
|
|
|
|
const upperAllowedTypes = ORG_LOGO_ALLOWED_TYPES.map((t) => t.toUpperCase());
|
|
const ORG_LOGO_ALLOWED_TYPES_LABEL = `${upperAllowedTypes
|
|
.slice(0, -1)
|
|
.join(", ")}, or ${upperAllowedTypes[upperAllowedTypes.length - 1]}`;
|
|
|
|
export interface IOrgLogoValidationResult {
|
|
valid: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
|
|
const detectImageType = (bytes: Uint8Array): ImageFileType | null => {
|
|
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
if (bytes.length >= 8 && PNG_MAGIC.every((b, i) => bytes[i] === b)) {
|
|
return "png";
|
|
}
|
|
// JPEG: FF D8 FF
|
|
if (
|
|
bytes.length >= 3 &&
|
|
bytes[0] === 0xff &&
|
|
bytes[1] === 0xd8 &&
|
|
bytes[2] === 0xff
|
|
) {
|
|
return "jpeg";
|
|
}
|
|
// WebP: "RIFF" at 0..3, "WEBP" at 8..11 (4..7 carries the file size).
|
|
if (
|
|
bytes.length >= 12 &&
|
|
bytes[0] === 0x52 &&
|
|
bytes[1] === 0x49 &&
|
|
bytes[2] === 0x46 &&
|
|
bytes[3] === 0x46 &&
|
|
bytes[8] === 0x57 &&
|
|
bytes[9] === 0x45 &&
|
|
bytes[10] === 0x42 &&
|
|
bytes[11] === 0x50
|
|
) {
|
|
return "webp";
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// validateOrgLogoFile sniffs the first 12 bytes of a File to verify
|
|
// it's one of the allowed image formats — the browser-reported
|
|
// file.type is based on extension, not content, so we can't trust it
|
|
// (e.g. a WebP saved with a `.png` extension).
|
|
export const validateOrgLogoFile = async (
|
|
file: File
|
|
): Promise<IOrgLogoValidationResult> => {
|
|
if (file.size > ORG_LOGO_MAX_SIZE_BYTES) {
|
|
return { valid: false, error: "Logo must be 100 KB or less." };
|
|
}
|
|
const headerBuf = await file.slice(0, 12).arrayBuffer();
|
|
const detected = detectImageType(new Uint8Array(headerBuf));
|
|
if (!detected || !ORG_LOGO_ALLOWED_TYPES.includes(detected)) {
|
|
return {
|
|
valid: false,
|
|
error: `Logo must be a ${ORG_LOGO_ALLOWED_TYPES_LABEL} file.`,
|
|
};
|
|
}
|
|
return { valid: true };
|
|
};
|