gigafibre-fsm/apps/client/src/pages/PaymentCancelPage.vue
louispaulb 2b3704761a feat(portal): passwordless magic-link login — retire ERPNext /login
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>
2026-04-22 13:25:28 -04:00

48 lines
1.8 KiB
Vue

<template>
<q-page padding class="flex flex-center">
<div style="max-width:520px;text-align:center">
<q-icon name="cancel" color="warning" size="80px" />
<div class="text-h5 text-weight-bold q-mt-lg">Paiement annule</div>
<div class="text-body1 text-grey-7 q-mt-sm">
Votre paiement n'a pas ete complete. Aucun montant n'a ete debite.
</div>
<div v-if="invoiceName" class="text-body2 text-grey-6 q-mt-sm">
Facture: <strong>{{ invoiceName }}</strong>
</div>
<!-- Authenticated -->
<div v-if="auth.authenticated" class="q-mt-xl q-gutter-sm">
<q-btn v-if="invoiceName" color="primary" unelevated label="Reessayer" icon="refresh"
@click="$router.push(`/invoices/${invoiceName}`)" />
<q-btn outline color="primary" label="Mes factures" icon="list" @click="$router.push('/invoices')" />
<q-btn flat color="grey-7" label="Accueil" icon="home" @click="$router.push('/')" />
</div>
<!-- Not authenticated -->
<div v-else class="q-mt-xl">
<q-banner rounded class="bg-blue-1 q-mb-md">
<template #avatar><q-icon name="info" color="primary" /></template>
<div class="text-body2">
Connectez-vous pour reessayer le paiement ou voir vos factures.
</div>
</q-banner>
<q-btn color="primary" unelevated label="Se connecter" icon="login" @click="goToLogin" />
</div>
</div>
</q-page>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { useMagicToken } from 'src/composables/useMagicToken'
const route = useRoute()
const router = useRouter()
const auth = useMagicToken()
const invoiceName = route.query.invoice || ''
function goToLogin () {
router.push({ name: 'login' })
}
</script>