Customers no longer authenticate with passwords. A POST to the hub's
/portal/request-link mints a 24h customer-scoped JWT and sends it via
email + SMS; the /#/login Vue page sits on top of this and a navigation
guard hydrates the Pinia store from the token on arrival.
Why now: legacy customer passwords are unsalted MD5 from the old PHP
system. Migrating hashes to PBKDF2 would still require a forced reset
for every customer, so it's simpler to drop passwords entirely. The
earlier Authentik forwardAuth attempt was already disabled on
client.gigafibre.ca; this removes the last vestige of ERPNext's
password form from the customer-facing path.
Hub changes:
- services/targo-hub/lib/portal-auth.js (new) — POST /portal/request-link
• 3-requests / 15-min per identifier rate limit (in-memory Map + timer)
• Lookup by email (email_id + email_billing), customer id (legacy +
direct name), or phone (cell + tel_home)
• Anti-enumeration: always 200 OK with redacted contact hint
• Email template with CTA button + raw URL fallback; SMS short form
- services/targo-hub/server.js — mount the new /portal/* router
Client changes:
- apps/client/src/pages/LoginPage.vue (new) — standalone full-page,
single identifier input, success chips, rate-limit banner
- apps/client/src/api/auth-portal.js (new) — thin fetch wrapper
- apps/client/src/stores/customer.js — hydrateFromToken() sync decoder,
stripTokenFromUrl (history.replaceState), init() silent Authentik
fallback preserved for staff impersonation
- apps/client/src/router/index.js — PUBLIC_ROUTES allowlist + guard
that hydrates from URL token before redirecting
- apps/client/src/api/auth.js — logout() clears store + bounces to
/#/login (no more Authentik redirect); 401 in authFetch is warn-only
- apps/client/src/composables/useMagicToken.js — thin read-through to
the store (no more independent decoding)
- PaymentSuccess/Cancel/CardAdded pages — goToLogin() uses router,
not window.location to id.gigafibre.ca
Infra:
- apps/portal/traefik-client-portal.yml — block /login and
/update-password on client.gigafibre.ca, redirect to /#/login.
Any stale bookmark or external link lands on the Vue page, not
ERPNext's password form.
Docs:
- docs/roadmap.md — Phase 4 checkbox flipped; MD5 migration item retired
- docs/features/billing-payments.md — replace MD5 reset note with
magic-link explainer
Online appointment booking (Plan B from the same discussion) is queued
for a follow-up session; this commit is Plan A only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
97 lines
3.2 KiB
YAML
97 lines
3.2 KiB
YAML
# Traefik dynamic route: client.gigafibre.ca → ERPNext (no Authentik)
|
|
#
|
|
# Purpose: Customer portal accessible without SSO.
|
|
# Customers authenticate via a passwordless magic link (email/SMS) — the
|
|
# Vue SPA's /#/login page posts to the hub, which mints a 24h JWT and
|
|
# sends it back through a link the customer clicks.
|
|
#
|
|
# Staff continue to use id.gigafibre.ca / Authentik — NOT this host.
|
|
#
|
|
# Deploy: copy to /opt/traefik/dynamic/ on 96.125.196.67
|
|
# 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:
|
|
# Main portal router — NO authentik middleware
|
|
client-portal:
|
|
rule: "Host(`client.gigafibre.ca`)"
|
|
entryPoints:
|
|
- web
|
|
- websecure
|
|
service: client-portal-svc
|
|
tls:
|
|
certResolver: letsencrypt
|
|
# Explicitly NO middlewares — customer auth is magic-link only
|
|
|
|
# Block ERPNext's password /login page — we replaced it with a
|
|
# passwordless flow at /#/login. Any stale bookmark or external link
|
|
# that hits /login is bounced into the SPA's login route so customers
|
|
# never see the password form (legacy MD5 hashes aren't supported
|
|
# in the new flow — see docs/features/customer-portal.md).
|
|
client-portal-block-login:
|
|
rule: "Host(`client.gigafibre.ca`) && (Path(`/login`) || Path(`/login/`))"
|
|
entryPoints:
|
|
- web
|
|
- websecure
|
|
service: client-portal-svc
|
|
middlewares:
|
|
- portal-redirect-magic-login
|
|
tls:
|
|
certResolver: letsencrypt
|
|
priority: 300
|
|
|
|
# Block ERPNext's password-reset page (equivalent path, same reason)
|
|
client-portal-block-update-password:
|
|
rule: "Host(`client.gigafibre.ca`) && PathPrefix(`/update-password`)"
|
|
entryPoints:
|
|
- web
|
|
- websecure
|
|
service: client-portal-svc
|
|
middlewares:
|
|
- portal-redirect-magic-login
|
|
tls:
|
|
certResolver: letsencrypt
|
|
priority: 300
|
|
|
|
# Block /desk access for portal users
|
|
client-portal-block-desk:
|
|
rule: "Host(`client.gigafibre.ca`) && PathPrefix(`/desk`)"
|
|
entryPoints:
|
|
- web
|
|
- websecure
|
|
service: client-portal-svc
|
|
middlewares:
|
|
- portal-redirect-home
|
|
tls:
|
|
certResolver: letsencrypt
|
|
priority: 200
|
|
|
|
middlewares:
|
|
# Redirect /login + /update-password → SPA's magic-link request page.
|
|
# The SPA lives at /assets/client-app/ (see apps/client/deploy.sh); its
|
|
# hash router serves /#/login.
|
|
portal-redirect-magic-login:
|
|
redirectRegex:
|
|
regex: ".*"
|
|
replacement: "https://client.gigafibre.ca/#/login"
|
|
permanent: false
|
|
|
|
# Redirect /desk attempts to portal home
|
|
portal-redirect-home:
|
|
redirectRegex:
|
|
regex: ".*"
|
|
replacement: "https://client.gigafibre.ca/me"
|
|
permanent: false
|
|
|
|
services:
|
|
# Same ERPNext frontend container, unique service name to avoid
|
|
# conflicts with Docker-label-defined services
|
|
client-portal-svc:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://erpnext-frontend-1:8080"
|