From 16af9c573a2a5f78369c8767610496f781403f6c Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Thu, 7 May 2026 19:41:30 +0800 Subject: [PATCH] feat: remove app name and logo customization feature Users can no longer customize the app name or logo. The branding API endpoints, permission, frontend UI, env vars (APP_NAME, MAX_LOGO_SIZE_KB), and all related tests are removed. Includes a migration to clean up branding data from existing databases. --- apps/api/drizzle/0010_remove_branding.sql | 14 + apps/api/drizzle/meta/_journal.json | 7 + apps/api/src/index.ts | 4 - apps/api/src/lib/env.ts | 2 - apps/api/src/openapi.yaml | 94 +-- apps/api/src/permissions.ts | 1 - apps/api/src/plugins/auth.ts | 1 - apps/api/src/routes/branding.ts | 106 ---- apps/api/src/routes/roles.ts | 1 - apps/docs/api/rest.md | 12 +- apps/docs/guide/configuration.md | 2 - apps/web/src/components/layout/app-layout.tsx | 51 +- apps/web/src/components/layout/sidebar.tsx | 9 +- .../components/settings/settings-dialog.tsx | 81 +-- docker/Dockerfile | 2 - packages/shared/src/i18n/en.ts | 10 - packages/shared/src/permissions.ts | 1 - tests/e2e/gui-settings-general.spec.ts | 53 +- tests/e2e/rbac-full.spec.ts | 2 +- tests/e2e/rbac.spec.ts | 4 +- tests/integration/api-key-scoping.test.ts | 2 +- tests/integration/api.test.ts | 4 +- tests/integration/branding.test.ts | 533 ------------------ tests/integration/rbac-matrix-full.test.ts | 10 - tests/integration/test-server.ts | 4 - tests/unit/api/effective-permissions.test.ts | 5 +- tests/unit/api/permissions.test.ts | 6 +- tests/unit/api/rbac-enforcement.test.ts | 4 +- tests/unit/api/utilities.test.ts | 4 - 29 files changed, 52 insertions(+), 977 deletions(-) create mode 100644 apps/api/drizzle/0010_remove_branding.sql delete mode 100644 apps/api/src/routes/branding.ts delete mode 100644 tests/integration/branding.test.ts diff --git a/apps/api/drizzle/0010_remove_branding.sql b/apps/api/drizzle/0010_remove_branding.sql new file mode 100644 index 00000000..c92d5df5 --- /dev/null +++ b/apps/api/drizzle/0010_remove_branding.sql @@ -0,0 +1,14 @@ +-- Remove branding:manage from built-in admin role permissions +UPDATE `roles` +SET `permissions` = REPLACE(`permissions`, '"branding:manage",', ''), + `updated_at` = unixepoch() +WHERE `id` = 'builtin-admin'; +--> statement-breakpoint +-- Remove branding:manage from any custom roles that have it +UPDATE `roles` +SET `permissions` = REPLACE(REPLACE(`permissions`, ',"branding:manage"', ''), '"branding:manage",', ''), + `updated_at` = unixepoch() +WHERE `id` != 'builtin-admin' AND `permissions` LIKE '%branding:manage%'; +--> statement-breakpoint +-- Clean up branding-related settings +DELETE FROM `settings` WHERE `key` IN ('customLogo', 'appName'); diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index c27590fe..1990be67 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1776855591000, "tag": "0009_analytics_consent", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1778300000000, + "tag": "0010_remove_branding", + "breakpoints": true } ] } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 98505306..c75837fb 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -21,7 +21,6 @@ import { analyticsRoutes } from "./routes/analytics.js"; import { apiKeyRoutes } from "./routes/api-keys.js"; import { auditLogRoutes } from "./routes/audit-log.js"; import { registerBatchRoutes } from "./routes/batch.js"; -import { brandingRoutes } from "./routes/branding.js"; import { docsRoutes } from "./routes/docs.js"; import { registerFeatureRoutes } from "./routes/features.js"; import { fileRoutes } from "./routes/files.js"; @@ -180,9 +179,6 @@ await analyticsRoutes(app); // Feature management routes (AI feature bundle install/uninstall) await registerFeatureRoutes(app); -// Branding routes (logo upload/serve/delete) -await brandingRoutes(app); - // Teams routes await teamsRoutes(app); diff --git a/apps/api/src/lib/env.ts b/apps/api/src/lib/env.ts index e3d009d5..296af595 100644 --- a/apps/api/src/lib/env.ts +++ b/apps/api/src/lib/env.ts @@ -26,7 +26,6 @@ const envSchema = z.object({ WORKSPACE_PATH: z.string().default("./tmp/workspace"), DEFAULT_THEME: z.enum(["light", "dark", "system"]).default("light"), DEFAULT_LOCALE: z.string().default("en"), - APP_NAME: z.string().default("snapotter"), CORS_ORIGIN: z.string().default(""), MAX_USERS: z.coerce.number().default(0), LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"), @@ -35,7 +34,6 @@ const envSchema = z.object({ MAX_PIPELINE_STEPS: z.coerce.number().default(0), MAX_CANVAS_PIXELS: z.coerce.number().default(0), MAX_SVG_SIZE_MB: z.coerce.number().default(0), - MAX_LOGO_SIZE_KB: z.coerce.number().default(500), MAX_SPLIT_GRID: z.coerce.number().default(100), MAX_PDF_PAGES: z.coerce.number().default(0), SESSION_DURATION_HOURS: z.coerce.number().default(168), diff --git a/apps/api/src/openapi.yaml b/apps/api/src/openapi.yaml index 9219a5ec..83a10284 100644 --- a/apps/api/src/openapi.yaml +++ b/apps/api/src/openapi.yaml @@ -39,8 +39,6 @@ tags: description: System-wide configuration (admin only for writes). - name: Teams description: Organize users into teams. - - name: Branding - description: Custom logo management. - name: Roles description: Custom role management with fine-grained permissions. - name: Audit @@ -5764,96 +5762,6 @@ paths: schema: $ref: "#/components/schemas/Error" - # ─── Branding ───────────────────────────────────────────────────────────── - - /api/v1/settings/logo: - get: - tags: [Branding] - summary: Get custom logo - security: [] - responses: - "200": - description: Logo image - content: - image/*: - schema: - type: string - format: binary - "404": - description: No custom logo set - post: - tags: [Branding] - summary: Upload custom logo (admin) - security: - - bearerAuth: [] - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: [logo] - properties: - logo: - type: string - format: binary - description: Logo image file - responses: - "200": - description: Logo uploaded - "400": - description: Invalid input - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - "401": - description: Authentication required - content: - application/json: - schema: - $ref: "#/components/schemas/UnauthorizedError" - "403": - description: Admin access required - content: - application/json: - schema: - $ref: "#/components/schemas/ForbiddenError" - delete: - tags: [Branding] - summary: Remove custom logo - description: Requires branding:manage permission. - security: - - bearerAuth: [] - responses: - "200": - description: Logo removed - content: - application/json: - schema: - type: object - properties: - ok: - type: boolean - "400": - description: Invalid input - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - "401": - description: Authentication required - content: - application/json: - schema: - $ref: "#/components/schemas/UnauthorizedError" - "403": - description: Admin access required - content: - application/json: - schema: - $ref: "#/components/schemas/ForbiddenError" - # ─── System (additional) ────────────────────────────────────────────────── /api/v1/config/auth: @@ -6417,7 +6325,7 @@ paths: Permission strings. Valid values: tools:use, files:own, files:all, apikeys:own, apikeys:all, pipelines:own, pipelines:all, settings:read, settings:write, users:manage, teams:manage, - branding:manage, features:manage, system:health, audit:read + features:manage, system:health, audit:read responses: "201": description: Role created diff --git a/apps/api/src/permissions.ts b/apps/api/src/permissions.ts index e566d76a..d3e6738a 100644 --- a/apps/api/src/permissions.ts +++ b/apps/api/src/permissions.ts @@ -17,7 +17,6 @@ const ROLE_PERMISSIONS: Record = { "settings:write", "users:manage", "teams:manage", - "branding:manage", "features:manage", "system:health", "audit:read", diff --git a/apps/api/src/plugins/auth.ts b/apps/api/src/plugins/auth.ts index 82e1f38b..03cdca1b 100644 --- a/apps/api/src/plugins/auth.ts +++ b/apps/api/src/plugins/auth.ts @@ -763,7 +763,6 @@ const PUBLIC_PATHS = [ "/api/auth/", "/api/v1/download/", "/api/v1/jobs/", - "/api/v1/settings/logo", "/api/docs", "/api/v1/openapi.yaml", ]; diff --git a/apps/api/src/routes/branding.ts b/apps/api/src/routes/branding.ts deleted file mode 100644 index 720c9a62..00000000 --- a/apps/api/src/routes/branding.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Branding routes — custom logo upload, serving, and deletion. - * - * POST /api/v1/settings/logo — Upload logo (admin only) - * GET /api/v1/settings/logo — Serve custom logo as PNG (public) - * DELETE /api/v1/settings/logo — Remove custom logo (admin only) - */ - -import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { eq } from "drizzle-orm"; -import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; -import sharp from "sharp"; -import { env } from "../config.js"; -import { db, schema } from "../db/index.js"; -import { ensureSharpCompat } from "../lib/heic-converter.js"; -import { requirePermission } from "../permissions.js"; - -const BRANDING_DIR = join(env.FILES_STORAGE_PATH, "branding"); -const LOGO_PATH = join(BRANDING_DIR, "logo.png"); -const maxLogoSize = env.MAX_LOGO_SIZE_KB * 1024; - -function upsertSetting(key: string, value: string): void { - const existing = db.select().from(schema.settings).where(eq(schema.settings.key, key)).get(); - if (existing) { - db.update(schema.settings) - .set({ value, updatedAt: new Date() }) - .where(eq(schema.settings.key, key)) - .run(); - } else { - db.insert(schema.settings).values({ key, value }).run(); - } -} - -export async function brandingRoutes(app: FastifyInstance): Promise { - // POST /api/v1/settings/logo — Upload logo (admin only) - app.post("/api/v1/settings/logo", async (request: FastifyRequest, reply: FastifyReply) => { - const admin = requirePermission("branding:manage")(request, reply); - if (!admin) return; - - const file = await request.file(); - if (!file) { - return reply.status(400).send({ error: "No file uploaded", code: "VALIDATION_ERROR" }); - } - - // Validate mimetype - if (!file.mimetype.startsWith("image/")) { - return reply.status(400).send({ error: "File must be an image", code: "VALIDATION_ERROR" }); - } - - // Read file buffer - const buffer = await file.toBuffer(); - - // Validate size - if (buffer.length > maxLogoSize) { - return reply.status(400).send({ - error: `Logo must be ${env.MAX_LOGO_SIZE_KB}KB or smaller`, - code: "VALIDATION_ERROR", - }); - } - - // Decode HEIC/HEIF if needed, then convert to PNG, resize to max 128x128 - const compatBuffer = await ensureSharpCompat(buffer); - const pngBuffer = await sharp(compatBuffer) - .resize(128, 128, { fit: "inside", withoutEnlargement: true }) - .png() - .toBuffer(); - - // Ensure branding directory exists - mkdirSync(BRANDING_DIR, { recursive: true }); - - // Write file - writeFileSync(LOGO_PATH, pngBuffer); - - // Upsert setting - upsertSetting("customLogo", "true"); - - return reply.send({ ok: true }); - }); - - // GET /api/v1/settings/logo — Serve logo (public, no auth required) - app.get("/api/v1/settings/logo", async (_request: FastifyRequest, reply: FastifyReply) => { - if (!existsSync(LOGO_PATH)) { - return reply.status(404).send({ error: "No custom logo set", code: "NOT_FOUND" }); - } - - const logoBuffer = readFileSync(LOGO_PATH); - return reply.type("image/png").send(logoBuffer); - }); - - // DELETE /api/v1/settings/logo — Remove logo (admin only) - app.delete("/api/v1/settings/logo", async (request: FastifyRequest, reply: FastifyReply) => { - const admin = requirePermission("branding:manage")(request, reply); - if (!admin) return; - - if (existsSync(LOGO_PATH)) { - unlinkSync(LOGO_PATH); - } - - upsertSetting("customLogo", "false"); - - return reply.send({ ok: true }); - }); - - app.log.info("Branding routes registered"); -} diff --git a/apps/api/src/routes/roles.ts b/apps/api/src/routes/roles.ts index c69e7615..a14ceca8 100644 --- a/apps/api/src/routes/roles.ts +++ b/apps/api/src/routes/roles.ts @@ -19,7 +19,6 @@ const ALL_PERMISSIONS: Permission[] = [ "settings:write", "users:manage", "teams:manage", - "branding:manage", "features:manage", "system:health", "audit:read", diff --git a/apps/docs/api/rest.md b/apps/docs/api/rest.md index 7c34f4e6..28cdb5e0 100644 --- a/apps/docs/api/rest.md +++ b/apps/docs/api/rest.md @@ -312,14 +312,6 @@ To auto-save a tool result to the library, include `fileId` in the settings payl | `PUT` | `/api/v1/teams/:id` | Admin (`teams:manage`) | Rename team | | `DELETE` | `/api/v1/teams/:id` | Admin (`teams:manage`) | Delete team (cannot delete default team or teams with members) | -## Branding - -| Method | Path | Access | Description | -|--------|------|--------|-------------| -| `POST` | `/api/v1/settings/logo` | Admin | Upload custom logo (max 500 KB, converted to 128×128 PNG) | -| `GET` | `/api/v1/settings/logo` | Public | Serve current logo | -| `DELETE` | `/api/v1/settings/logo` | Admin | Remove custom logo | - ## Settings Runtime key-value configuration (read by any authenticated user, write by admin only). @@ -330,7 +322,7 @@ Runtime key-value configuration (read by any authenticated user, write by admin | `PUT` | `/api/v1/settings` | Bulk update settings (JSON body with key-value pairs) | | `GET` | `/api/v1/settings/:key` | Get a specific setting by key | -Known keys: `disabledTools` (JSON array of tool IDs), `enableExperimentalTools` (bool string), `loginAttemptLimit` (number), `customLogo` (managed via branding endpoint). +Known keys: `disabledTools` (JSON array of tool IDs), `enableExperimentalTools` (bool string), `loginAttemptLimit` (number). ## Roles @@ -343,7 +335,7 @@ Custom role management with granular permissions. | `PUT` | `/api/v1/roles/:id` | Admin (`users:manage`) | Update a custom role (cannot modify built-in roles) | | `DELETE` | `/api/v1/roles/:id` | Admin (`users:manage`) | Delete a custom role (cannot delete built-in roles; affected users revert to `user` role) | -Available permissions: `tools:use`, `files:own`, `files:all`, `apikeys:own`, `apikeys:all`, `pipelines:own`, `pipelines:all`, `settings:read`, `settings:write`, `users:manage`, `teams:manage`, `branding:manage`, `features:manage`, `system:health`, `audit:read`. +Available permissions: `tools:use`, `files:own`, `files:all`, `apikeys:own`, `apikeys:all`, `pipelines:own`, `pipelines:all`, `settings:read`, `settings:write`, `users:manage`, `teams:manage`, `features:manage`, `system:health`, `audit:read`. ## Audit Log diff --git a/apps/docs/guide/configuration.md b/apps/docs/guide/configuration.md index 6c5a8bf0..c752426d 100644 --- a/apps/docs/guide/configuration.md +++ b/apps/docs/guide/configuration.md @@ -47,7 +47,6 @@ All configuration is done through environment variables. Every variable has a se | `MAX_PIPELINE_STEPS` | `0` (no limit) | Maximum number of steps in a pipeline. Set to 0 for no limit. | | `MAX_CANVAS_PIXELS` | `0` (no limit) | Maximum canvas size in pixels for output images. Set to 0 for no limit. | | `MAX_SVG_SIZE_MB` | `0` (unlimited) | Maximum SVG file size in megabytes. Set to 0 for unlimited. | -| `MAX_LOGO_SIZE_KB` | `500` | Maximum custom branding logo size in kilobytes. | | `MAX_SPLIT_GRID` | `100` | Maximum grid dimension for the image split tool. | | `MAX_PDF_PAGES` | `0` (unlimited) | Maximum number of PDF pages for PDF-to-image conversion. Set to 0 for unlimited. | @@ -62,7 +61,6 @@ All configuration is done through environment variables. Every variable has a se | Variable | Default | Description | |---|---|---| -| `APP_NAME` | `SnapOtter` | Display name shown in the UI. | | `DEFAULT_THEME` | `light` | Default theme for new sessions. `light` or `dark`. | | `DEFAULT_LOCALE` | `en` | Default interface language. | diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx index 5e9c57f9..3e7d2651 100644 --- a/apps/web/src/components/layout/app-layout.tsx +++ b/apps/web/src/components/layout/app-layout.tsx @@ -1,8 +1,7 @@ import { FolderOpen, LayoutGrid, Menu, Settings as SettingsIcon, Workflow, X } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { useMobile } from "@/hooks/use-mobile"; -import { apiGet } from "@/lib/api"; import { cn } from "@/lib/utils"; import { useConnectionStore } from "@/stores/connection-store"; import { Dropzone } from "../common/dropzone"; @@ -25,16 +24,9 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout const [helpOpen, setHelpOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const isMobile = useMobile(); - const [customLogo, setCustomLogo] = useState(false); const connectionStatus = useConnectionStore((s) => s.status); const bannerVisible = connectionStatus !== "connected"; - useEffect(() => { - apiGet<{ settings: Record }>("/v1/settings") - .then((data) => setCustomLogo(data.settings.customLogo === "true")) - .catch(() => {}); - }, []); - return (
setSettingsOpen(true)} onHelpClick={() => setHelpOpen(true)} - customLogo={customLogo} /> )} @@ -61,20 +52,12 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout />
- {customLogo ? ( - Logo - ) : ( -
- - - SnapOtter - -
- )} +
+ + + SnapOtter + +
- {customLogo ? ( - Logo - ) : ( -
- - - SnapOtter - -
- )} +
+ + + SnapOtter + +
)} diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index 4e05b53c..08252637 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -29,8 +29,6 @@ interface SidebarProps { onNavClick?: () => void; /** When true, renders in expanded mode (for mobile overlay). */ expanded?: boolean; - /** Whether a custom logo is set. */ - customLogo?: boolean; } export function Sidebar({ @@ -38,7 +36,6 @@ export function Sidebar({ onHelpClick, onNavClick, expanded = false, - customLogo = false, }: SidebarProps) { const location = useLocation(); @@ -103,11 +100,7 @@ export function Sidebar({ return (