Skip to content

feat: Add support for sub-path based serving#1430

Open
mdrkrg wants to merge 10 commits into
sysadminsmedia:mainfrom
mdrkrg:feat/path-based-serving
Open

feat: Add support for sub-path based serving#1430
mdrkrg wants to merge 10 commits into
sysadminsmedia:mainfrom
mdrkrg:feat/path-based-serving

Conversation

@mdrkrg
Copy link
Copy Markdown
Contributor

@mdrkrg mdrkrg commented Apr 14, 2026

What type of PR is this?

  • feature

What this PR does / why we need it:

Add support for serving the app under a subpath (e.g., https://example.com/homebox/).

This enables admins who cannot control a *.example.com domain DNS but want to deploy multiple apps to serve all applications under different URL prefixes.

Backend

  • conf.go: Add APIBase and FrontendBase config fields (default /).
  • routes.go: Mount API routes at APIBase + /api + /v1 instead of hardcoded /api/v1.
  • oidc.go:
    • Update getBaseURL() to include APIBase for correct callback URLs
    • Fix OIDC error redirect to use FrontendBase instead of hardcoded /
    • Change cookie Path from / to u.Path so cookies work under subpaths

Frontend

  • nuxt.config.ts: Use NUXT_APP_BASE_URL and DEV_API_BASE environment variable. Updates:
    • app.baseURL for subpath routing
    • Nitro dev proxy to proxy at subpath, using DEV_API_BASE
    • PWA accounting for subpath (workbox, manifest.start_url)
  • New use-app-base.ts: Returns config.app.baseURL for use in components
  • url.ts: Include base URL in route generation
  • frontend/pages/index.vue: Use useAppBase() to construct the OIDC login redirect URL
  • use-server-event.ts: Use useAppBase() for correct WebSocket connection URL
  • Invite links: Use useAppBase() in JoinModal.vue and invites.vue for correct invite URLs

Which issue(s) this PR fixes:

None, new feature.

Special notes for your reviewer:

  • Added APIBase and FrontendBase in config. The APIBase is used for both route registration and URL generation. The FrontendBase is only used for URL generation.
  • Cookie path is dynamically set based on the request's base URL to ensure OIDC flow works correctly under subpaths. Although simply setting Path: "/" works.

Testing

  • New features only tested working in dev
  • All existing tests pass

Summary by CodeRabbit

  • New Features

    • App can be deployed at custom base paths; frontend base and API base are configurable.
    • API endpoints, WebSocket connections, and OIDC authentication redirects now honor the configured app base.
    • Invite links and login flows build URLs from the configured app base.
  • Chores

    • Docker build context updated to include the frontend environment file when present.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Walkthrough

Introduces configurable app base path across backend and frontend: new WebConfig.AppBase with normalization, routes and OIDC flows updated to use AppBase, cookie paths derived from computed base URL, and multiple frontend URL builders and runtime settings adjusted to respect app.baseURL.

Changes

Cohort / File(s) Summary
Docker & Frontend env
\.dockerignore, frontend/.env.example
Stop ignoring frontend/.env in Docker context; add NUXT_APP_BASE_URL example entry.
Backend config
backend/internal/sys/config/conf.go
Add WebConfig.AppBase and normalizePath() to enforce leading/trailing slashes on configured base path.
Backend routing & OIDC
backend/app/api/routes.go, backend/app/api/handlers/v1/controller.go, backend/app/api/handlers/v1/v1_ctrl_auth.go, backend/app/api/providers/oidc.go
Mount v1 subrouter using Web.AppBase; pass WebConfig into OIDC provider; compute base URL including AppBase; set/clear OIDC cookies with Path from parsed base URL; auth redirects use configured AppBase.
Frontend runtime & build config
frontend/nuxt.config.ts, frontend/.env.example
Introduce normalized app.baseURL from env, apply to Nitro dev proxy, PWA patterns, theme script and manifest start URL.
Frontend URL plumbing & plugin
frontend/lib/api/base/urls.ts, frontend/plugins/api-base.ts, frontend/composables/use-app-base.ts
Add parts.base, extend overrideParts(host,prefix,base), plugin initializes parts with app.baseURL, and provide useAppBase() composable.
Frontend usage updates
frontend/composables/use-server-events.ts, frontend/components/Collection/JoinModal.vue, frontend/pages/collection/.../invites.vue, frontend/pages/index.vue
Use useAppBase()/app.baseURL for WebSocket URL, invite links, and OIDC login redirect; replace window.host/domain concatenation with base-aware URL construction.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser as Browser
  participant Frontend as Frontend (Nuxt app.baseURL)
  participant Backend as Backend (API, routes use AppBase)
  participant IdP as OIDC Provider

  Browser->>Frontend: Click "Login with OIDC"
  Frontend->>Backend: GET {app.baseURL}api/v1/users/login/oidc
  Backend->>IdP: Redirect to IdP (redirect_uri includes API base)
  IdP->>Browser: Redirect to IdP login
  Browser->>IdP: Authenticate
  IdP->>Backend: Callback to {app.baseURL}api/v1/users/oidc/callback
  Backend->>Backend: handleCallback (set/clear cookies with Path from parsed base URL)
  Backend->>Browser: Redirect to {app.baseURL}home or error query
  Browser->>Frontend: Render post-login page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • tankerkiller125
  • katosdev
  • tonyaellie

Security Recommendations

  • Validate AppBase input: disallow absolute URLs with host, strip or reject .. segments, and enforce allowed characters to prevent open-redirects and path traversal.
  • Ensure all computed OIDC redirect URIs are exact matches registered with the IdP; do not allow attacker-controlled AppBase values to alter origins.
  • Confirm cookie scope: review Path, SameSite, Secure, and HttpOnly attributes when changing cookie Path to avoid unintended cross-path access.
  • Test PWA/runtime-caching regexes to ensure API endpoints are not incorrectly cached or exposed under the new base prefix.
  • Add unit/integration tests for path normalization, route mounting with non-root AppBase, OIDC flow, and WebSocket connections under prefixed bases.

Poem

🌐 A base path set, both front and back align,
Routes and cookies march in tidy line,
Redirects follow where the app-base goes,
Links and sockets find their proper home,
Small change, big compass — onward we go! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add support for sub-path based serving' accurately summarizes the main change—adding subpath support for app deployment—making it clear and specific.
Description check ✅ Passed The description provides comprehensive coverage of all required sections: PR type (feature), detailed explanation of changes by component, no issues fixed, special notes, and testing approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added ⬆️ enhancement New feature or request go Pull requests that update Go code labels Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/api/handlers/v1/v1_ctrl_auth.go (1)

190-238: ⚠️ Potential issue | 🔴 Critical

Use consistent, configurable cookie paths for local authentication to match OIDC provider and prevent cookie leakage in subpath deployments.

Both setCookies() and unsetCookies() hardcode Path: "/", while the OIDC provider correctly uses dynamic Path: u.Path extracted from the request. This inconsistency creates security and functional issues:

  1. Cookie isolation in subpath deployments: With Path: "/", cookies are scoped to the domain root, making them accessible to all applications at that domain (e.g., if deployed at /homebox/, tokens leak to /other-app/).
  2. OIDC/local auth mismatch: The two auth paths set cookies with different paths, breaking interoperability in subpath deployments.
  3. Logout failures: unsetCookies() also uses Path: "/", so clearing cookies may fail if the original path differs.

Since ctrl.config.Web.APIBase is already available (and used elsewhere in this file at line 346), update both functions to use the configured API base path instead of hardcoding "/". This mirrors the OIDC provider's approach and ensures proper cookie isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/handlers/v1/v1_ctrl_auth.go` around lines 190 - 238,
setCookies currently hardcodes Path: "/" which leaks cookies across subpaths;
update setCookies to use the configured API base path (ctrl.config.Web.APIBase)
for the Cookie.Path (mirroring the OIDC provider's use of u.Path) so cookies are
scoped to the app subpath. Do the same in unsetCookies: replace Path: "/" with
the same ctrl.config.Web.APIBase value used elsewhere in this file so setting
and clearing cookies use the identical, configurable path and prevent cross-app
leakage and logout failures. Ensure both functions reference
ctrl.config.Web.APIBase (or a normalized variant if needed) for all cookies
including the attachment token and session cookie.
🧹 Nitpick comments (1)
frontend/pages/index.vue (1)

22-22: Remove manual import - composables are auto-imported.

As per coding guidelines, composables from the composables/ directory are auto-imported by Nuxt. This manual import is unnecessary.

♻️ Proposed fix
-  import { useAppBase } from "~/composables/use-app-base";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/pages/index.vue` at line 22, The manual import of the composable is
unnecessary: remove the line importing useAppBase from
"~/composables/use-app-base" in frontend/pages/index.vue and rely on Nuxt's
auto-imported composables so references to useAppBase in this file continue to
work; ensure no other explicit imports of useAppBase remain in this file and run
the dev build to verify no unresolved symbol errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/nuxt.config.ts`:
- Around line 3-4: The env-provided base paths (baseURL and devAPIBase) may lack
a trailing slash which causes later concatenation like base + "api" to produce
"/homeboxapi"; update the code that reads process.env.NUXT_APP_BASE_URL and
process.env.DEV_API_BASE to normalize values to always end with a single "/"
(treat empty or undefined as "/"), e.g., implement a small helper
(normalizeBasePath) and apply it to baseURL and devAPIBase so downstream string
concatenation (references to baseURL and devAPIBase in this file) is safe.
- Around line 66-70: The PWA API regex is constructed using baseURL with its
trailing slash, producing a double slash (e.g., ^//api); update the RegExp
builders used in navigateFallbackDenylist and runtimeCaching → urlPattern to
strip any trailing slash from baseURL before concatenating "/api" (e.g., use
baseURL with .replace to remove a trailing "/" or otherwise normalize it) so the
resulting patterns match "/api" correctly for both the default "/" and subpath
bases; ensure you change both navigateFallbackDenylist and the runtimeCaching
urlPattern occurrences.

In `@frontend/plugins/api-base.ts`:
- Around line 8-10: The plugin currently uses config.app.baseURL when calling
overrideParts in defineNuxtPlugin; change it to read a dedicated runtime public
API base (e.g., config.public.apiBase) instead: update the useRuntimeConfig()
usage and pass config.public.apiBase into overrideParts rather than
config.app.baseURL, and ensure nuxt.config exposes public.apiBase so frontend
API requests are decoupled from app.baseURL.

---

Outside diff comments:
In `@backend/app/api/handlers/v1/v1_ctrl_auth.go`:
- Around line 190-238: setCookies currently hardcodes Path: "/" which leaks
cookies across subpaths; update setCookies to use the configured API base path
(ctrl.config.Web.APIBase) for the Cookie.Path (mirroring the OIDC provider's use
of u.Path) so cookies are scoped to the app subpath. Do the same in
unsetCookies: replace Path: "/" with the same ctrl.config.Web.APIBase value used
elsewhere in this file so setting and clearing cookies use the identical,
configurable path and prevent cross-app leakage and logout failures. Ensure both
functions reference ctrl.config.Web.APIBase (or a normalized variant if needed)
for all cookies including the attachment token and session cookie.

---

Nitpick comments:
In `@frontend/pages/index.vue`:
- Line 22: The manual import of the composable is unnecessary: remove the line
importing useAppBase from "~/composables/use-app-base" in
frontend/pages/index.vue and rely on Nuxt's auto-imported composables so
references to useAppBase in this file continue to work; ensure no other explicit
imports of useAppBase remain in this file and run the dev build to verify no
unresolved symbol errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: adc5c173-257f-45ad-a232-43fd0d69e0d6

📥 Commits

Reviewing files that changed from the base of the PR and between e911e6a and 1557876.

📒 Files selected for processing (15)
  • .dockerignore
  • backend/app/api/handlers/v1/controller.go
  • backend/app/api/handlers/v1/v1_ctrl_auth.go
  • backend/app/api/providers/oidc.go
  • backend/app/api/routes.go
  • backend/internal/sys/config/conf.go
  • frontend/.env.example
  • frontend/components/Collection/JoinModal.vue
  • frontend/composables/use-app-base.ts
  • frontend/composables/use-server-events.ts
  • frontend/lib/api/base/urls.ts
  • frontend/nuxt.config.ts
  • frontend/pages/collection/index/invites.vue
  • frontend/pages/index.vue
  • frontend/plugins/api-base.ts

Comment thread frontend/nuxt.config.ts Outdated
Comment thread frontend/nuxt.config.ts Outdated
Comment thread frontend/plugins/api-base.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
backend/app/api/providers/oidc.go (2)

458-465: Same defensive error handling recommended in handleCallback.

The url.Parse error is also ignored here. For consistency and security robustness, handle the error.

🛡️ Defensive error handling
 	// Helper to clear state cookie using computed domain
 	baseURL := p.getBaseURL(r)
-	u, _ := url.Parse(baseURL)
+	u, err := url.Parse(baseURL)
+	if err != nil {
+		log.Err(err).Str("baseURL", baseURL).Msg("failed to parse base URL for OIDC callback")
+		return services.UserAuthTokenDetail{}, fmt.Errorf("internal server error")
+	}
 	domain := u.Hostname()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/providers/oidc.go` around lines 458 - 465, The url.Parse
error in handleCallback is currently ignored; update handleCallback to capture
and handle the error returned by url.Parse(p.getBaseURL(r))—check the error, log
or return a descriptive error (or wrap it) and avoid proceeding with a
nil/invalid URL; ensure you still compute domain using u.Hostname() only after a
successful parse and fall back to noPort(r.Host) when appropriate, similar to
the defensive handling used elsewhere in the provider code.

405-411: Consider handling the url.Parse error for defensive security.

The error from url.Parse(baseURL) is discarded. While baseURL is constructed internally and should always be valid, explicitly handling the error prevents unexpected behavior if future changes introduce edge cases. A malformed URL could result in an empty u.Path, potentially scoping cookies to / instead of the intended subpath.

🛡️ Defensive error handling
 	// Get base URL from request
 	baseURL := p.getBaseURL(r)
-	u, _ := url.Parse(baseURL)
+	u, err := url.Parse(baseURL)
+	if err != nil {
+		log.Err(err).Str("baseURL", baseURL).Msg("failed to parse base URL for OIDC flow")
+		return services.UserAuthTokenDetail{}, fmt.Errorf("internal server error")
+	}
 	domain := u.Hostname()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/providers/oidc.go` around lines 405 - 411, Handle the error
returned by url.Parse when parsing baseURL (from p.getBaseURL(r)) instead of
discarding it; check the returned error and, on parse failure, fall back to a
safe default such as using noPort(r.Host) for domain or logging the parse error
before using the fallback so that u.Path isn't relied on when malformed. Update
the code around url.Parse(baseURL) and the subsequent use of u (variable u,
domain, noPort, getBaseURL, and r.Host) to perform this defensive check and
graceful fallback.
backend/internal/sys/config/conf.go (1)

147-161: Address the static analysis finding: use compound assignment.

The linter flagged line 158. This is a minor style issue but should be fixed to pass CI.

♻️ Use compound assignment operator
 func normalizePath(p string) string {
 	if p == "" {
 		return "/"
 	}
 	if !strings.HasPrefix(p, "/") {
 		p = "/" + p
 	}
 	if !strings.HasSuffix(p, "/") {
-		p = p + "/"
+		p += "/"
 	}
 	return p
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/internal/sys/config/conf.go` around lines 147 - 161, The linter
flagged a non-compound assignment in normalizePath; update the concatenation "p
= p + \"/\"" to use the compound operator (p += "/") in the normalizePath
function so behavior is unchanged and the style issue is resolved; ensure you
only change that assignment and keep the existing checks for leading/trailing
slashes in function normalizePath.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/app/api/providers/oidc.go`:
- Around line 458-465: The url.Parse error in handleCallback is currently
ignored; update handleCallback to capture and handle the error returned by
url.Parse(p.getBaseURL(r))—check the error, log or return a descriptive error
(or wrap it) and avoid proceeding with a nil/invalid URL; ensure you still
compute domain using u.Hostname() only after a successful parse and fall back to
noPort(r.Host) when appropriate, similar to the defensive handling used
elsewhere in the provider code.
- Around line 405-411: Handle the error returned by url.Parse when parsing
baseURL (from p.getBaseURL(r)) instead of discarding it; check the returned
error and, on parse failure, fall back to a safe default such as using
noPort(r.Host) for domain or logging the parse error before using the fallback
so that u.Path isn't relied on when malformed. Update the code around
url.Parse(baseURL) and the subsequent use of u (variable u, domain, noPort,
getBaseURL, and r.Host) to perform this defensive check and graceful fallback.

In `@backend/internal/sys/config/conf.go`:
- Around line 147-161: The linter flagged a non-compound assignment in
normalizePath; update the concatenation "p = p + \"/\"" to use the compound
operator (p += "/") in the normalizePath function so behavior is unchanged and
the style issue is resolved; ensure you only change that assignment and keep the
existing checks for leading/trailing slashes in function normalizePath.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cd06096f-f8a0-4593-ba87-cd996851ec95

📥 Commits

Reviewing files that changed from the base of the PR and between d9847d9 and 0eaa6fe.

📒 Files selected for processing (6)
  • backend/app/api/handlers/v1/v1_ctrl_auth.go
  • backend/app/api/providers/oidc.go
  • backend/app/api/routes.go
  • backend/internal/sys/config/conf.go
  • frontend/.env.example
  • frontend/nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/app/api/routes.go
  • backend/app/api/handlers/v1/v1_ctrl_auth.go
  • frontend/.env.example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⬆️ enhancement New feature or request go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant