185 lines
5.4 KiB
Markdown
185 lines
5.4 KiB
Markdown
|
|
# Configurable Base Path for SaaS App
|
||
|
|
|
||
|
|
## Problem
|
||
|
|
|
||
|
|
Logto uses many root-level paths (`/sign-in`, `/register`, `/consent`, `/social`, `/api/interaction`, `/api/experience`, `/assets`, etc.) that conflict with the SaaS app's catch-all routing. Enumerating Logto's paths in Traefik is fragile and keeps growing.
|
||
|
|
|
||
|
|
## Solution
|
||
|
|
|
||
|
|
Move the SaaS app to a configurable base path (default: `/platform`). Logto becomes the Traefik catch-all. Zero path enumeration — any path Logto adds in the future just works.
|
||
|
|
|
||
|
|
## Routing
|
||
|
|
|
||
|
|
| Path | Target | Priority |
|
||
|
|
|------|--------|----------|
|
||
|
|
| `/platform/*` | cameleer-saas:8080 | default |
|
||
|
|
| `/server/*` | cameleer-server-ui:80 | default |
|
||
|
|
| `/*` | logto:3001 (catch-all) | 1 (lowest) |
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
```env
|
||
|
|
# .env
|
||
|
|
CONTEXT_PATH=/platform # Change to /saas, /app, etc. No rebuild needed.
|
||
|
|
```
|
||
|
|
|
||
|
|
## Implementation
|
||
|
|
|
||
|
|
### 1. Spring Boot — `application.yml`
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
server:
|
||
|
|
servlet:
|
||
|
|
context-path: ${CONTEXT_PATH:/platform}
|
||
|
|
```
|
||
|
|
|
||
|
|
Spring automatically prefixes all endpoints. Controllers, SecurityConfig matchers, interceptor patterns — all relative to context-path. No changes needed in Java code.
|
||
|
|
|
||
|
|
### 2. Vite — `ui/vite.config.ts`
|
||
|
|
|
||
|
|
Build with relative base so assets work from any prefix:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
build: {
|
||
|
|
outDir: 'dist',
|
||
|
|
emptyOutDir: true,
|
||
|
|
assetsDir: '_app',
|
||
|
|
// removed: base (default '/' for dev, entrypoint injects <base> for production)
|
||
|
|
},
|
||
|
|
```
|
||
|
|
|
||
|
|
Change `base` to `'./'` so index.html references become relative:
|
||
|
|
```html
|
||
|
|
<!-- Before: <script src="/_app/index.js"> (absolute, breaks with prefix) -->
|
||
|
|
<!-- After: <script src="_app/index.js"> (relative, works from any base) -->
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Container entrypoint — inject `<base href>`
|
||
|
|
|
||
|
|
Create `docker/entrypoint.sh`:
|
||
|
|
```sh
|
||
|
|
#!/bin/sh
|
||
|
|
# Inject <base href> into index.html for runtime base path support
|
||
|
|
CONTEXT_PATH="${CONTEXT_PATH:-/platform}"
|
||
|
|
sed -i "s|<head>|<head><base href=\"${CONTEXT_PATH}/\">|" /app/static/index.html
|
||
|
|
exec java -jar /app/app.jar
|
||
|
|
```
|
||
|
|
|
||
|
|
In Dockerfile (or docker-compose override for dev):
|
||
|
|
```yaml
|
||
|
|
entrypoint: ["sh", "/app/entrypoint.sh"]
|
||
|
|
```
|
||
|
|
|
||
|
|
For dev mode (mounted dist), the `docker-compose.dev.yml` entrypoint runs the sed on the mounted file.
|
||
|
|
|
||
|
|
### 4. Frontend — derive base path at runtime
|
||
|
|
|
||
|
|
**`ui/src/config.ts`** — use `document.baseURI`:
|
||
|
|
```ts
|
||
|
|
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
|
||
|
|
// basePath = "/platform"
|
||
|
|
|
||
|
|
fetch(basePath + '/api/config')
|
||
|
|
```
|
||
|
|
|
||
|
|
**`ui/src/api/client.ts`** — dynamic API base:
|
||
|
|
```ts
|
||
|
|
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
|
||
|
|
const API_BASE = basePath + '/api';
|
||
|
|
```
|
||
|
|
|
||
|
|
**`ui/src/main.tsx`** — router basename:
|
||
|
|
```tsx
|
||
|
|
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '') || '/';
|
||
|
|
<BrowserRouter basename={basePath}>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Docker Compose — Traefik labels
|
||
|
|
|
||
|
|
**cameleer-saas:**
|
||
|
|
```yaml
|
||
|
|
labels:
|
||
|
|
- traefik.http.routers.saas.rule=PathPrefix(`${CONTEXT_PATH:-/platform}`)
|
||
|
|
- traefik.http.routers.saas.entrypoints=websecure
|
||
|
|
- traefik.http.routers.saas.tls=true
|
||
|
|
- traefik.http.services.saas.loadbalancer.server.port=8080
|
||
|
|
```
|
||
|
|
|
||
|
|
**logto (catch-all):**
|
||
|
|
```yaml
|
||
|
|
labels:
|
||
|
|
- traefik.http.routers.logto.rule=PathPrefix(`/`)
|
||
|
|
- traefik.http.routers.logto.priority=1
|
||
|
|
- traefik.http.routers.logto.entrypoints=websecure
|
||
|
|
- traefik.http.routers.logto.tls=true
|
||
|
|
- traefik.http.services.logto.loadbalancer.server.port=3001
|
||
|
|
```
|
||
|
|
|
||
|
|
Remove all the enumerated Logto paths — Logto is now the catch-all.
|
||
|
|
|
||
|
|
### 6. Logto ENDPOINT
|
||
|
|
|
||
|
|
Logto's ENDPOINT stays at root (no prefix):
|
||
|
|
```yaml
|
||
|
|
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||
|
|
```
|
||
|
|
|
||
|
|
OIDC issuer = `https://domain.com/oidc`. Same domain as the SPA.
|
||
|
|
|
||
|
|
### 7. Bootstrap — redirect URIs
|
||
|
|
|
||
|
|
Update redirect URIs to include the context path:
|
||
|
|
```sh
|
||
|
|
SPA_REDIRECT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/callback\"]"
|
||
|
|
SPA_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/login\"]"
|
||
|
|
```
|
||
|
|
|
||
|
|
Pass `CONTEXT_PATH` to the bootstrap container.
|
||
|
|
|
||
|
|
### 8. Tests — `application-test.yml`
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
server:
|
||
|
|
servlet:
|
||
|
|
context-path: /platform
|
||
|
|
```
|
||
|
|
|
||
|
|
Test MockMvc paths are relative to context-path, so existing test paths (`/api/tenants`, etc.) continue to work without changes.
|
||
|
|
|
||
|
|
## Files to modify
|
||
|
|
|
||
|
|
- `src/main/resources/application.yml` — add context-path property
|
||
|
|
- `src/main/resources/application-test.yml` — add context-path for tests
|
||
|
|
- `ui/vite.config.ts` — `base: './'` for relative assets
|
||
|
|
- `ui/src/config.ts` — derive base path from `document.baseURI`
|
||
|
|
- `ui/src/api/client.ts` — dynamic `API_BASE`
|
||
|
|
- `ui/src/main.tsx` — `BrowserRouter basename` from `document.baseURI`
|
||
|
|
- `docker/entrypoint.sh` — NEW, injects `<base href>` into index.html
|
||
|
|
- `docker-compose.yml` — Traefik labels (SaaS at `/platform`, Logto catch-all), pass `CONTEXT_PATH` to bootstrap
|
||
|
|
- `docker/logto-bootstrap.sh` — context path in redirect URIs
|
||
|
|
|
||
|
|
## Files that do NOT change
|
||
|
|
|
||
|
|
- All 11 Java controllers — Spring context-path handles prefix transparently
|
||
|
|
- `SecurityConfig.java` — matchers are relative to context-path
|
||
|
|
- `WebConfig.java` — interceptor pattern relative to context-path
|
||
|
|
- `ui/src/api/hooks.ts` — uses centralized `API_BASE`
|
||
|
|
- All test files — MockMvc is context-path aware
|
||
|
|
|
||
|
|
## Customer experience
|
||
|
|
|
||
|
|
```env
|
||
|
|
# .env
|
||
|
|
PUBLIC_HOST=cameleer.mycompany.com
|
||
|
|
PUBLIC_PROTOCOL=https
|
||
|
|
CONTEXT_PATH=/platform
|
||
|
|
|
||
|
|
# DNS: 1 record
|
||
|
|
# cameleer.mycompany.com → server IP
|
||
|
|
|
||
|
|
# docker compose up -d
|
||
|
|
# SaaS at https://cameleer.mycompany.com/platform/
|
||
|
|
# Logto at https://cameleer.mycompany.com/ (login, OIDC)
|
||
|
|
# Server UI at https://cameleer.mycompany.com/server/
|
||
|
|
```
|