Skip to content

feat(branding): customize login page title and logo via env vars (#3773)#3781

Merged
Yeraze merged 4 commits into
mainfrom
feat/custom-login-branding
Jun 26, 2026
Merged

feat(branding): customize login page title and logo via env vars (#3773)#3781
Yeraze merged 4 commits into
mainfrom
feat/custom-login-branding

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #3773. Adds native support for custom login-page branding via environment variables, so operators deploying MeshMonitor as a dashboard for a community or organization can rebrand the login screen without forking and rebuilding the Docker image.

Variable Effect Default
CUSTOM_TITLE Overrides the "MeshMonitor" heading on the login page MeshMonitor
CUSTOM_LOGO_URL Overrides the inline SVG logo with your own image built-in SVG

Both are optional and gracefully fall back to the defaults when unset.

How it works

The login page renders client-side, so branding is delivered at runtime through the existing public /api/auth/status endpoint (same channel already used for oidcEnabled / localAuthDisabled). No new endpoint, no build-time baking.

  • environment.ts — parses both vars. CUSTOM_LOGO_URL is validated to allow only http(s)://, protocol-relative, data:image/, or same-origin relative paths; anything else (e.g. javascript:) is logged and ignored.
  • authRoutes.ts/status exposes customTitle / customLogoUrl (null when unset) in all three response branches.
  • AuthContext.tsx — optional fields added to AuthStatus.
  • LoginPage.tsx — renders an <img> when a logo URL is set, otherwise the original SVG; the <h1> uses the custom title or MeshMonitor.
  • Docs.env.example and docs/configuration/index.md updated.

Testing

  • New authRoutes.test.ts cases: null-by-default, values exposed when set, unsafe-scheme rejection.
  • Full backend route + env-config suite passes (success: true, 0 failures); AuthContext.test.tsx passes (120/120).
  • No new tsc errors introduced over baseline.

🤖 Generated with Claude Code

Add CUSTOM_TITLE and CUSTOM_LOGO_URL environment variables so operators
deploying MeshMonitor for a community/organization can rebrand the login
page without forking and rebuilding the image.

- environment.ts: parse both vars; validate CUSTOM_LOGO_URL scheme
  (http(s)/data:image/relative only, rejecting javascript: etc.)
- authRoutes /status: expose branding (null when unset) pre-auth so the
  login page can read it
- AuthContext: optional customTitle/customLogoUrl on AuthStatus
- LoginPage: render custom logo image + title, falling back to the
  built-in SVG and "MeshMonitor" heading when unset
- docs + .env.example: document the new variables
- tests: cover default/null, set values, and unsafe-scheme rejection

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(branding) - Custom Login Page Branding

  • Gather context and read changed files
  • Review code quality and best practices
  • Review security concerns
  • Review test coverage
  • Post final feedback

Overall this is a clean, well-scoped feature. The approach of piggybacking on the existing /api/auth/status endpoint is smart — no new routes, and the branding is available pre-auth where it's needed. Here's my detailed feedback:

Unrelated Files in this PR

Three files in the diff have nothing to do with login branding and should be removed before merge:

  • .claude/agent-memory/docker-dev-deployer/LATEST_DEPLOYMENT.md — AI agent memory from a different branch (feat/automation-token-hints). This is internal dev tooling state, not source code.
  • .claude/agent-memory/docker-dev-deployer/worktree_deployment_2026-06-24.md — Same, agent memory artifact.
  • docs/internal/dev-notes/MCP_SERVER_PLAN.md — A design document for a future MCP server feature, unrelated to login branding.

These are likely worktree merge artifacts. Please remove them before merging.


Security

CUSTOM_LOGO_URL validation allows data:image/ URIs (environment.ts:718-723)

data:image/svg+xml,... is accepted by the regex. SVG data URIs can contain embedded <script> tags or <a href="javascript:..."> elements. When this URL is used as the src of an <img> element, browsers typically sandbox SVG in <img> and don't execute scripts — but this is browser-dependent and has historically been exploited. Consider either:

  • Rejecting data:image/svg specifically (only allow data:image/png, data:image/jpeg, data:image/gif, data:image/webp), or
  • Adding a data: block entirely and documenting that inline logos should be hosted externally.

CUSTOM_TITLE has no sanitization

customTitle is rendered directly in a React <h1> tag (LoginPage.tsx:155). Since it goes through JSX, React will escape it automatically — this is fine as-is. Worth noting for documentation: HTML tags in CUSTOM_TITLE will appear as literal text.

Logo URL is served pre-auth

The branding values are returned to unauthenticated users in the /status response. This is intentional and correct — that's the point. Just confirming this was a deliberate design choice, which it clearly was.


Code Quality

Double-trim on customTitle is redundant (environment.ts:705 and LoginPage.tsx:35)

customTitleRaw is already .trim()'d in environment.ts before being stored. LoginPage.tsx:35 then does authStatus?.customTitle?.trim() again. The second trim is harmless but unnecessary.

customLogoUrl URL validation uses a simple relative-path check (environment.ts:722)

v.startsWith('/')

This allows paths like /api/some-endpoint which would hit your own backend. That's probably fine in practice (only a server operator sets this), but it's worth being explicit in documentation that relative paths are resolved against the server's origin.

authRoutes.ts spreads branding into all three response branches (lines 86, 112, 129)

The approach of building branding once and spreading it is clean. Minor note: if you ever add more branding fields, you only need to update one place. ✓

AuthContext.tsx error fallback explicitly sets both fields to null (lines 129-130)

customTitle: null,
customLogoUrl: null,

This is correct and explicit — good practice.


Test Coverage

The new tests in authRoutes.test.ts (lines 1087–1139) cover the three main cases well:

  • ✅ Null by default
  • ✅ Values exposed when set
  • ✅ Unsafe scheme (javascript:) rejected

Missing test: whitespace-only CUSTOM_TITLE

customTitleRaw || undefined means CUSTOM_TITLE=" " (all spaces) correctly becomes undefined after trimming. A test asserting this would be a nice safety net, though it's a minor edge case.

Missing test: data:image/svg+xml acceptance

Given the security concern above about SVG data URIs, a test either asserting it's accepted or rejected (depending on the decision made) would be valuable to document the intent.

AuthStatus interface uses optional fields (AuthContext.tsx:46-47)

customTitle?: string | null;
customLogoUrl?: string | null;

Using ?: means TypeScript won't flag access points that don't handle the undefined case. Given the backend always sends these (as null when unset), consider making them non-optional (customTitle: string | null) to match the runtime guarantee.


Minor

The login-logo-image CSS class sets both width/height in CSS (.login-logo-image in LoginPage.css:44-48) and as HTML attributes on the <img> tag (LoginPage.tsx:128-129). The HTML attributes are redundant when CSS is loaded, but they do serve as a layout hint before CSS loads — this is acceptable.


Summary

Action required before merge:

  1. Remove the three unrelated files (agent-memory and MCP plan doc)
  2. Consider restricting data:image/svg+xml in CUSTOM_LOGO_URL validation

Nice to have:

  • Make customTitle/customLogoUrl non-optional in AuthStatus interface
  • Add edge-case tests for whitespace title and SVG data URIs

The core feature logic is solid — the URL validation, fallback behavior, and test structure are all well done.

…/tests

- Remove three unrelated files that `git add -A` swept into the branch
  (.claude/agent-memory/* and docs/internal/dev-notes/MCP_SERVER_PLAN.md)
- CUSTOM_LOGO_URL: reject data:image/svg+xml (SVG can embed scripts);
  whitelist raster data: URIs (png/jpeg/gif/webp/avif) only
- AuthStatus.customTitle/customLogoUrl are now non-optional (string | null)
  to match the runtime guarantee that /status always sends them
- Add edge-case tests: whitespace-only title, SVG data URI rejected,
  raster data URI accepted
- Docs/.env.example note the SVG exclusion and relative-path origin

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(branding) - Custom Login Page Branding

  • Gather context and read changed files
  • Review code quality and best practices
  • Review security concerns
  • Review test coverage
  • Post final feedback

This is a follow-up review after the cb2ca4c commit addressed the previous round of feedback. Overall the feature is in good shape. Here's my current assessment:

What Was Fixed ✅

All major issues from the prior review have been addressed:

  • Unrelated files: The commit message claims to drop stray files, but LATEST_DEPLOYMENT.md and worktree_deployment_2026-06-24.md are still present in the branch (checked with glob). docs/internal/dev-notes/MCP_SERVER_PLAN.md is also still present. These still need to be removed before merge.
  • SVG data URI rejection: environment.ts:723 now correctly uses data:image/(png|jpe?g|gif|webp|avif) — SVG is explicitly excluded. ✅
  • AuthStatus interface uses non-optional fields: AuthContext.tsx:49-50 now has customTitle: string | null and customLogoUrl: string | null (no ?:). ✅
  • Tests added: whitespace title (' '), SVG data URI rejection, and raster data URI acceptance are all tested. ✅

Remaining Issue: Unrelated Files Still in PR

Despite cb2ca4c's description, these three files are still present on the branch and will be included in the merge:

  • .claude/agent-memory/docker-dev-deployer/LATEST_DEPLOYMENT.md — deployment notes from feat/automation-token-hints
  • .claude/agent-memory/docker-dev-deployer/worktree_deployment_2026-06-24.md — deployment notes from a geofence worktree
  • docs/internal/dev-notes/MCP_SERVER_PLAN.md — future MCP server design doc

Please run git rm on these and push before merging.


Minor Code Quality Notes

Double-trim still present (LoginPage.tsx:35-36)

const customTitle = authStatus?.customTitle?.trim() || 'MeshMonitor';
const customLogoUrl = authStatus?.customLogoUrl?.trim() || null;

environment.ts:705 already trims customTitleRaw before storing it. The .trim() calls here are redundant (harmless, but unnecessary noise). Given the backend always stores a pre-trimmed value or null, these can safely be simplified to:

const customTitle = authStatus?.customTitle || 'MeshMonitor';
const customLogoUrl = authStatus?.customLogoUrl || null;

Relative path validation allows any server path (environment.ts:724)

v.startsWith('/')

This accepts paths like /api/some-endpoint or /api/auth/status. In practice, only a server operator sets this, so it's unlikely to be misused. A quick documentation note in .env.example clarifying that relative paths resolve against the server origin would be helpful for operators who might not know this.


Security: No New Concerns

  • XSS via customTitle: JSX escapes this automatically in <h1>{customTitle}</h1>. ✅
  • Logo URL injection: The allow-list regex at environment.ts:720-724 is correct and well-commented. The javascript: scheme is blocked; SVG data URIs are blocked. ✅
  • Pre-auth exposure: Intentional and appropriate — branding must be visible before login. ✅

Test Coverage: Solid

The authRoutes.test.ts branding suite (lines 1087–1170) now covers:

  • ✅ Null by default
  • ✅ Values exposed when set
  • javascript: scheme rejected
  • ✅ Whitespace-only title treated as unset
  • data:image/svg+xml rejected
  • ✅ Raster data URI accepted

No gaps remain in the test coverage for the core validation paths.


Summary

Must fix before merge:

  • Remove the three unrelated files (LATEST_DEPLOYMENT.md, worktree_deployment_2026-06-24.md, MCP_SERVER_PLAN.md) — they are still on the branch despite the commit message saying otherwise.

Nice to have (not blocking):

  • Remove redundant .trim() calls in LoginPage.tsx:35-36
  • Add a note in .env.example that relative paths in CUSTOM_LOGO_URL resolve against the server origin

The core feature logic, validation, fallback behavior, and test structure are all solid. ✅

These were untracked working-tree artifacts at branch creation that a
prior `git add -A` swept in. Untrack them (kept on disk locally).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(branding) - Custom Login Page Branding

  • Gather context and read changed files
  • Review code quality and best practices
  • Review security concerns
  • Review test coverage
  • Post final feedback

This is a solid update — the previous review feedback has been addressed well. Here's the current state:

What Was Fixed Since the Previous Review

Security: data:image/svg+xml is now explicitly rejected (environment.ts:723). The regex data:image/(png|jpe?g|gif|webp|avif) correctly allows only raster formats. ✅

TypeScript types: customTitle and customLogoUrl are now non-optional (string | null) in AuthContext.tsx:49-50, matching the runtime guarantee. ✅

Test coverage: All four edge cases are now covered (lines 1140–1168):

  • Whitespace-only CUSTOM_TITLE treated as unset ✅
  • SVG data URI rejected ✅
  • Raster data URI accepted ✅
  • javascript: scheme rejected ✅

Remaining Issue: Unrelated Files Still Present

The .claude/agent-memory/docker-dev-deployer/ directory still exists on this branch, but checking the files there, they appear to be standard agent-memory files from this repo (not from a different branch). Looking at the previous review's concern more carefully — the three files called out (LATEST_DEPLOYMENT.md, worktree_deployment_2026-06-24.md, docs/internal/dev-notes/MCP_SERVER_PLAN.md) no longer appear in the current diff. These were apparently already removed in the latest push.


Remaining Minor Issues

Double-trim on customTitle is still redundant (LoginPage.tsx:35)

const customTitle = authStatus?.customTitle?.trim() || 'MeshMonitor';

customTitle is already trimmed and normalized to null in environment.ts:705-708 before reaching the client. The second .trim() here is harmless but unnecessary. It could be simplified to:

const customTitle = authStatus?.customTitle || 'MeshMonitor';

alt text on custom logo always uses customTitle (LoginPage.tsx:127)

<img src={customLogoUrl} ... />

This is fine as a default, but if the operator sets a CUSTOM_TITLE that contains HTML-like characters (e.g., My Org & Community), React will correctly escape it in the attribute. No action needed — just confirming this is handled correctly by JSX.

customLogoUrl trim on client is also unnecessary (LoginPage.tsx:36)

const customLogoUrl = authStatus?.customLogoUrl?.trim() || null;

The value from the server is already trimmed and validated. Same minor redundancy as above.


Code Quality: Good

  • The branding object built once and spread into all three /status response branches is clean (authRoutes.ts:51-54, then spread at lines 86, 112, 129)
  • The warning log message in environment.ts:726 is clear and actionable
  • The CSS object-fit: contain on .login-logo-image is the right choice for arbitrary logo dimensions

Summary

This PR is ready to merge. The core security concern (SVG data URIs) was fixed, types were tightened, and edge-case tests were added. The only remaining items are cosmetic (unnecessary .trim() calls on client-derived values that are already normalized). These don't affect correctness or security.

@Yeraze

Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the thorough review! Addressed in cb2ca4c and 4874ec4:

Action required:

  1. Removed the three stray files (.claude/agent-memory/*, docs/internal/dev-notes/MCP_SERVER_PLAN.md) — they were untracked working-tree artifacts a git add -A swept in. Net PR diff is now just the 9 branding-related files.
  2. data:image/svg+xml now rejected. CUSTOM_LOGO_URL validation whitelists raster data: URIs only (png/jpeg/gif/webp/avif); SVG data URIs fall back to the default logo. New test covers it.

Nice to have:

  • AuthStatus.customTitle/customLogoUrl are now non-optional (string | null) to match the runtime guarantee that /status always sends them. Mock updated.
  • Added edge-case tests: whitespace-only CUSTOM_TITLE → null, SVG data URI rejected, raster data URI accepted.
  • Docs/.env.example updated to note the SVG exclusion and that relative paths resolve against the server origin.

Intentionally left as-is:

  • The defensive .trim() in LoginPage.tsx is redundant with the backend trim, but it's a harmless guard against an untrimmed value arriving over the wire — keeping it.
  • Redundant width/height on both the <img> attributes and CSS — kept deliberately as a pre-CSS layout hint (you noted this is acceptable).

The server already trims and normalizes CUSTOM_TITLE/CUSTOM_LOGO_URL
(null when unset), so the client-side .trim() was dead code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(branding) - Custom Login Page Branding

  • Gather context and read changed files
  • Review code quality and best practices
  • Review security concerns
  • Review test coverage
  • Post final feedback

This is now the fourth review cycle. The latest commit (145d4df) resolved the last outstanding "nice to have" (redundant client-side .trim() calls). This PR is in good shape and ready to merge. Details below.

What Was Addressed in the Latest Commit ✅

Redundant .trim() removed from LoginPage.tsx:36-37

// Before (redundant):
const customTitle = authStatus?.customTitle?.trim() || 'MeshMonitor';
const customLogoUrl = authStatus?.customLogoUrl?.trim() || null;

// After (clean):
const customTitle = authStatus?.customTitle || 'MeshMonitor';
const customLogoUrl = authStatus?.customLogoUrl || null;

The comment at line 33–35 explains the reasoning clearly — the server already trims and normalizes these values. ✅


No Remaining Issues

Unrelated files: All three previously flagged files (LATEST_DEPLOYMENT.md, worktree_deployment_2026-06-24.md, MCP_SERVER_PLAN.md) are confirmed absent from the branch diff. The diff is now exactly the 9 branding-related files. ✅

Security — SVG data URIs blocked (environment.ts:723): The regex data:image/(png|jpe?g|gif|webp|avif)[;,] correctly rejects data:image/svg+xml. ✅

Security — javascript: scheme blocked: The allow-list only passes http(s)://, //, raster data:image/, and same-origin relative paths. ✅

TypeScript types (AuthContext.tsx:49-50): customTitle: string | null and customLogoUrl: string | null (non-optional) match the runtime contract. Error fallback path (AuthContext.tsx:132-133) explicitly sets both to null. ✅

Test coverage (authRoutes.test.ts:1087-1170): All six cases covered — null default, values set, javascript: rejected, whitespace-only title treated as unset, SVG data URI rejected, raster data URI accepted. ✅

Documentation (.env.example, docs/configuration/index.md): Both updated with correct descriptions, including the SVG exclusion and relative-path note. ✅


Minor Observations (Non-blocking)

customTitleProvided / customLogoUrlProvided are exported but not consumed (environment.ts return object). These flags are tracked internally but only customTitle and customLogoUrl values are used by authRoutes.ts. The extra fields are harmless but could be pruned in a follow-up if desired.

alt text on the logo image uses customTitle (LoginPage.tsx:128). This is the right default — the title describes the logo. React JSX handles any special characters in customTitle automatically via attribute escaping. ✅

width/height on <img> are redundant with CSS (LoginPage.tsx:129-130, LoginPage.css:44-45). This was acknowledged as intentional (pre-CSS layout hint). ✅


Summary

All previous action-required items have been resolved. The feature is complete, secure, well-tested, and documented. Ready to merge.

@Yeraze Yeraze merged commit e13057c into main Jun 26, 2026
20 checks passed
@Yeraze Yeraze deleted the feat/custom-login-branding branch June 26, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Feature Request: Support for Custom Branding (Title & Logo) on Login Page via Environment Variables

1 participant