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>
33 lines
1.3 KiB
JavaScript
33 lines
1.3 KiB
JavaScript
/**
|
|
* Portal passwordless auth — talks to targo-hub /portal/*.
|
|
*
|
|
* Flow:
|
|
* 1. Customer lands on /#/login (no token, no session).
|
|
* 2. Types email or phone, hits "Recevoir mon lien".
|
|
* 3. requestPortalLink() POSTs the identifier to the hub.
|
|
* 4. Hub looks up the Customer, mints a 24h JWT, sends via SMS + email.
|
|
* 5. Customer clicks the link in their inbox → portal.gigafibre.ca/#/?token=JWT.
|
|
* 6. useMagicToken() decodes it on page load, hydrates the customer store.
|
|
*
|
|
* The hub always returns 200 OK (anti-enumeration), so the only
|
|
* non-success response the UI should handle is 429 (rate limit).
|
|
*/
|
|
const HUB = location.hostname === 'localhost' ? 'http://localhost:3300' : 'https://msg.gigafibre.ca'
|
|
|
|
export async function requestPortalLink (identifier) {
|
|
const r = await fetch(HUB + '/portal/request-link', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ identifier }),
|
|
})
|
|
const data = await r.json().catch(() => ({}))
|
|
if (r.status === 429) {
|
|
const err = new Error(data.message || 'Trop de tentatives. Réessayez plus tard.')
|
|
err.code = 'rate_limit'
|
|
err.retryAfterSec = data.retry_after_sec || 900
|
|
throw err
|
|
}
|
|
if (!r.ok) throw new Error(data.error || `Hub ${r.status}`)
|
|
return data
|
|
}
|