Topology clarification: - portal.gigafibre.ca = standalone nginx container serving /opt/client-app/ (the actual Vue SPA). This is the real customer portal. - client.gigafibre.ca = ERPNext frontend (exposes Frappe's password login form — dead-end UX, legacy MD5 attack surface). Changes: - apps/client/deploy.sh: target /opt/client-app/ directly with DEPLOY_BASE=/ (was uploading into ERPNext's /assets/client-app/, which nothing serves). Atomic stage-and-swap + docker restart so the nginx bind-mount picks up the new inode. - apps/portal/traefik-client-portal.yml: replace per-path /login and /desk blocks on client.gigafibre.ca with a catch-all 307 to portal.gigafibre.ca. Old bookmarks, old invoice links, and in-flight SMS all end up on the Vue SPA instead of Frappe's password page. - apps/ops/package-lock.json: sync to include html5-qrcode transitive deps so `npm ci` in deploy.sh works from a clean checkout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
53 lines
2.2 KiB
YAML
53 lines
2.2 KiB
YAML
# Traefik dynamic route: client.gigafibre.ca → redirect to portal.gigafibre.ca
|
|
#
|
|
# Background (2026-04-22):
|
|
# The customer portal Vue SPA now lives at portal.gigafibre.ca (served by a
|
|
# standalone nginx:alpine container from /opt/client-app/; Traefik label
|
|
# defined inline on that container). Customers authenticate with a 24h
|
|
# passwordless magic-link JWT — no Authentik, no ERPNext /login.
|
|
#
|
|
# The legacy host client.gigafibre.ca used to point at the ERPNext frontend
|
|
# directly, exposing Frappe's password login form. That's an attack surface
|
|
# we don't want (legacy MD5 hashes, enumerable accounts) and a dead-end UX
|
|
# (the Vue SPA isn't served there). This config retires client.gigafibre.ca
|
|
# by permanent-redirecting all traffic on that host to portal.gigafibre.ca,
|
|
# so stale bookmarks, old invoice links, and SMS that went out before the
|
|
# cut-over still land customers on the right front door.
|
|
#
|
|
# Staff continue to use id.gigafibre.ca / Authentik — untouched.
|
|
#
|
|
# Deploy:
|
|
# scp traefik-client-portal.yml root@96.125.196.67:/opt/traefik/dynamic/
|
|
# (Traefik auto-reloads dynamic config — no restart needed)
|
|
#
|
|
# DNS: *.gigafibre.ca wildcard already resolves to 96.125.196.67
|
|
# TLS: Let's Encrypt auto-provisions cert for client.gigafibre.ca
|
|
|
|
http:
|
|
routers:
|
|
# Catch-all: every request on client.gigafibre.ca → portal.gigafibre.ca
|
|
# (preserves path so /invoice/INV-123 still lands somewhere sensible on
|
|
# the new SPA, which has a hash router — paths without a leading # just
|
|
# land on the SPA root and the user re-navigates from there).
|
|
client-portal-legacy-redirect:
|
|
rule: "Host(`client.gigafibre.ca`)"
|
|
entryPoints:
|
|
- web
|
|
- websecure
|
|
service: noop@internal
|
|
middlewares:
|
|
- portal-legacy-redirect
|
|
tls:
|
|
certResolver: letsencrypt
|
|
priority: 100
|
|
|
|
middlewares:
|
|
# 302 (not 301) during the test phase so browsers don't cache the
|
|
# redirect permanently — we can still iterate on hostnames if needed.
|
|
# Flip `permanent: true` once the portal host is stable.
|
|
portal-legacy-redirect:
|
|
redirectRegex:
|
|
regex: "^https?://client\\.gigafibre\\.ca/(.*)"
|
|
replacement: "https://portal.gigafibre.ca/$1"
|
|
permanent: false
|