Skip to main content

Customer Portal — Epics & User Stories Report

Status: populated Owner: Product Engineering (Frontend) Last updated: 2026-04-18


EP-CUST-01 — Customer Authentication & Session Management

Goal: Allow customers to securely sign in, maintain sessions, and sign out.

US-CUST-01-01 — Customer Login (revised v1.1 — Keycloak OIDC PKCE)

Revision note (2026-04-20): Per ADR-0002 / PLT-ADR-009, Keycloak is the base/default IdP and auth-service is the canonical identity surface. The original Firebase-based login flow has been replaced with Keycloak OIDC PKCE. Firebase remains only as a feature-flagged legacy fallback for the migration window per EP-AUTH-05 (Firebase retirement).

As a customer account holder, I want to sign in with my email and password (via the platform-managed Keycloak login), So that I can access the self-service portal.

Acceptance Criteria:

  • Login page at /login renders a "Sign in" CTA that initiates an OIDC Authorization Code flow with PKCE against auth-service (/v1/auth/oidc/authorize).
  • PKCE: client generates code_verifier (43–128 chars, RFC 7636) and code_challenge = BASE64URL(SHA256(verifier)); state is a CSRF-resistant random nonce.
  • On callback at /auth/callback, the code is exchanged for the platform JWT pair via POST /v1/auth/oidc/token; state is verified.
  • Platform JWT (RS256, issued by auth-service) is verified using the JWKS at /.well-known/jwks.json (cached 5 min).
  • customer role claim is verified; login rejected with explicit message if claim missing.
  • JWT and refresh token stored in httpOnly; Secure; SameSite=Strict cookies (encrypted with the BFF session key).
  • User is redirected to /dashboard on success; deep-link return path preserved across login.
  • Invalid credentials surface the upstream Keycloak error message after one redirect (no credentials proxied through the portal).
  • Rate-limit (429) from Kong shows "Too many attempts. Try again in N seconds." with the upstream Retry-After honoured.
  • Tenant configured for external SSO (per auth.tenant_identity_providers) is transparently brokered through Keycloak IdP mapper; the portal does not branch UI per IdP.
  • Feature flag LEGACY_FIREBASE_LOGIN_ENABLED defaults to false; when true (legacy migration window), a secondary "Sign in with Firebase" CTA is shown — calls the legacy POST /v1/auth/firebase exchange via auth-service Firebase legacy provider per EP-AUTH-05.
  • E2E test tests/e2e/login-keycloak.spec.ts covers: happy path, missing role, expired state, replay-attack on code, legacy Firebase fallback when flag is on.
  • Telemetry: customer_portal_login_total{provider} counter; customer_portal_login_latency_seconds histogram; OTel span customer-portal.login linked to the upstream auth-service.token-exchange span.

Story Points: 5


US-CUST-01-02 — Session Refresh

As a logged-in customer, I want my session to refresh automatically, So that I am not logged out unexpectedly during active use.

Acceptance Criteria:

  • Next.js middleware detects expired JWT and attempts silent refresh
  • Refresh calls POST /v1/auth/refresh with the __refresh cookie
  • New JWT and refresh token are set as cookies transparently
  • If refresh fails, user is redirected to /login?reason=session_expired
  • No user-visible disruption on successful refresh

Story Points: 3


US-CUST-01-03 — Logout

As a logged-in customer, I want to sign out, So that my session is terminated on this device.

Acceptance Criteria:

  • "Sign out" button in the navigation header
  • POST /api/auth/logout clears __session and __refresh cookies
  • Firebase signOut() is called client-side
  • User is redirected to /login after logout
  • Subsequent navigation to protected pages redirects to /login

Story Points: 2


EP-CUST-02 — API Key Management

Goal: Allow customers to create, view, and revoke API keys for programmatic access.

US-CUST-02-01 — View API Keys

As a customer, I want to see all my active API keys, So that I can manage my integrations.

Acceptance Criteria:

  • /api-keys page lists all keys with: name, scopes, status, lastUsedAt, createdAt
  • Revoked keys shown with visual distinction (greyed out)
  • Raw key value is never shown in the list
  • Empty state shown when no keys exist

Story Points: 3


US-CUST-02-02 — Create API Key

As a customer, I want to create a new API key with specific scopes, So that I can give my application access to only what it needs.

Acceptance Criteria:

  • "Create API Key" button opens a modal
  • Modal has fields: key name (required), scope checkboxes (sms:send, sms:read, billing:read)
  • On submit, POST /v1/api-keys is called
  • On success, modal transitions to "Key Created" state showing rawKey
  • Raw key displayed with "Copy to clipboard" button
  • Warning displayed: "This key will not be shown again"
  • On modal close, rawKey is cleared from component state
  • Key list re-fetches after creation

Story Points: 5


US-CUST-02-03 — Revoke API Key

As a customer, I want to revoke an API key that is no longer needed, So that I can prevent unauthorised use of an old key.

Acceptance Criteria:

  • "Revoke" button on each active key row
  • Confirmation dialog: "Are you sure you want to revoke this key? This cannot be undone."
  • On confirm, DELETE /v1/api-keys/{keyId} is called
  • Revoked key shown with revoked status badge in the list
  • Error toast if revocation fails

Story Points: 3


EP-CUST-03 — Test SMS Sender

Goal: Allow customers to send a single test SMS directly from the portal.

US-CUST-03-01 — Send Test SMS

As a customer, I want to send a test SMS from the portal, So that I can verify my integration is working without writing code.

Acceptance Criteria:

  • /send-test page has fields: to (E.164), from (Sender ID), body
  • Client-side validation: E.164 regex for to, non-empty body
  • Character counter shown; warning at > 160 chars (multi-part SMS)
  • On submit, POST /v1/messages called via BFF route
  • Success: show messageId with link to message detail
  • Error: display API error message
  • Form resets after successful submission

Story Points: 5


EP-CUST-04 — Message Logs

Goal: Allow customers to inspect their sent message history.

US-CUST-04-01 — View Message Log

As a customer, I want to see a list of messages I have sent, So that I can monitor delivery and investigate failures.

Acceptance Criteria:

  • /messages page shows paginated table of messages
  • Columns: messageId, to, from, status badge, submittedAt, deliveredAt
  • Filter panel: date range, status multi-select, to field, from field
  • Filters reflected in URL search params (shareable links)
  • Pagination: "Load more" appends next page
  • Clicking a row navigates to /messages/{messageId} detail page

Story Points: 8


US-CUST-04-02 — Message Detail

As a customer, I want to view full details of a single message, So that I can understand why a message failed.

Acceptance Criteria:

  • /messages/{messageId} renders full message detail
  • Shows: message body, operator name, all status timestamps, error code (if failed)
  • Error codes are rendered with human-readable descriptions
  • Back button returns to message list with filters preserved

Story Points: 3


EP-CUST-05 — Webhook Configuration

Goal: Allow customers to configure webhook endpoints for delivery notifications.

US-CUST-05-01 — Manage Webhooks

As a customer, I want to configure webhook endpoints, So that my application is notified when messages are delivered or fail.

Acceptance Criteria:

  • /webhooks page lists configured webhooks with URL, events, status
  • "Add Webhook" opens a modal: HTTPS URL (required), event type checkboxes
  • On create, signing secret displayed once in modal
  • Inline edit: update URL and events
  • Delete with confirmation dialog
  • HTTPS-only URL validation; http:// URLs rejected

Story Points: 8


EP-CUST-06 — Billing Overview

Goal: Allow customers to view invoices and current usage.

US-CUST-06-01 — Billing Page

As a customer, I want to view my billing invoices and current usage, So that I can track my spend and download invoices.

Acceptance Criteria:

  • /billing page has two tabs: "Invoices" and "Usage"
  • Invoices tab: paginated table with period, amount, status
  • "Download PDF" button opens pre-signed invoice PDF URL in new tab
  • Usage tab: current period metrics (messages sent, delivered, delivery rate, credit balance)
  • Overdue invoices highlighted with overdue badge

Story Points: 5


EP-CUST-07 — Session Security Hardening (CSP, COEP, COOP, Trusted-Types, SRI)

Goal: Bring the portal up to enterprise web-security baseline with strict transport, content, and resource-integrity headers so that XSS, clickjacking, and supply-chain script injection are mitigated by the browser.

US-CUST-07-01 — Strict Content Security Policy with Nonces

As a security engineer, I want every page to ship a CSP with per-request nonces and no unsafe-inline, So that XSS via injected <script> is blocked by the browser even if our HTML escaping fails.

Acceptance Criteria:

  • Middleware emits Content-Security-Policy header with default-src 'self'; script-src 'self' 'nonce-{n}'; style-src 'self' 'nonce-{n}'; img-src 'self' data: https:; connect-src 'self' https://api.ghasi.io; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests;.
  • Per-request nonce is propagated to all <script>/<style> tags via Next.js headers() and React Server Component context.
  • CSP report-only mirror header Content-Security-Policy-Report-Only posts to /csp-report for one release before enforcement.
  • No unsafe-inline, no unsafe-eval, no broad https: in script-src.
  • E2E test verifies the header on /, /login, /dashboard, /messages.

Story Points: 5


US-CUST-07-02 — COOP / COEP / CORP Isolation

As a security engineer, I want the portal isolated as a cross-origin browsing context group, So that Spectre-class side-channel and cross-origin reads are blocked.

Acceptance Criteria:

  • Cross-Origin-Opener-Policy: same-origin.
  • Cross-Origin-Embedder-Policy: require-corp.
  • Cross-Origin-Resource-Policy: same-site.
  • All third-party assets are self-hosted or carry Cross-Origin-Resource-Policy: cross-origin upstream.
  • Lighthouse a11y/perf scores unchanged after enabling.

Story Points: 3


US-CUST-07-03 — Trusted Types Policy

As a frontend engineer, I want the browser to refuse string-to-DOM sinks unless they go through a Trusted Types policy, So that unsafe DOM injections become impossible at runtime.

Acceptance Criteria:

  • CSP includes require-trusted-types-for 'script'; trusted-types ghasi-portal nextjs;.
  • App registers a single ghasi-portal policy that escapes to DOMPurify for any HTML sink.
  • Build step fails on dangerouslySetInnerHTML outside the audited wrapper.
  • Trusted Types violation report goes to /csp-report.

Story Points: 3


US-CUST-07-04 — Sub-Resource Integrity for Self-Hosted Assets

As a supply-chain auditor, I want every script and stylesheet served with an integrity SHA-384 attribute, So that silent CDN tampering is detected by the browser.

Acceptance Criteria:

  • Build emits integrity for every static asset reference.
  • CDN headers include Cache-Control: public, max-age=31536000, immutable and Cross-Origin-Resource-Policy: same-site.
  • CI gate fails if any <script src> lacks integrity.
  • SRI mismatch in production logs to Sentry with build ID.

Story Points: 3


US-CUST-07-05 — Standard Hardening Headers

As a security engineer, I want the standard hardening headers present and validated, So that clickjacking, MIME-sniffing, and stale TLS protection are correct.

Acceptance Criteria:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload.
  • X-Content-Type-Options: nosniff.
  • Referrer-Policy: strict-origin-when-cross-origin.
  • Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=().
  • Mozilla Observatory grade A+.
  • securityheaders.com grade A.

Story Points: 2


EP-CUST-08 — Customer Success Surface (QBR exports, TAM tooling, success metrics)

Goal: Give Customer Success / TAM (Technical Account Manager) staff the data and tooling they need for quarterly business reviews and proactive intervention without raw DB access.

US-CUST-08-01 — QBR PDF Export per Tenant

As a TAM, I want a one-click QBR PDF for any of my assigned tenants, So that I can run a quarterly business review without manually assembling charts.

Acceptance Criteria:

  • POST /v1/internal/qbr/{tenantId}?period=2026-Q1 returns 202 with a job ID; polling endpoint returns the signed S3 URL when ready (≤ 60 s).
  • PDF includes: traffic by month, delivery rate by MNO, compliance score trajectory, hold-queue activity, top sender IDs, top destinations, spend, credit balance, SLA-credit accruals.
  • PDF is signed with the QBR signing key (Vault secret/cust/qbr-signing-key) and embeds the signature.
  • Tenant-isolation: TAM can only export tenants assigned to them via auth.tam_tenant_assignments.
  • Audit event customer-portal.qbr.exported.v1 published to NATS.

Story Points: 5


US-CUST-08-02 — Customer Health Score Card

As a TAM, I want a per-tenant health score visible in the admin view, So that I can prioritise outreach to declining accounts.

Acceptance Criteria:

  • Health score (0–100) computed from: 30-day delivery rate, compliance hold rate, payment timeliness, support-ticket volume, NPS.
  • Score colour-coded (red < 60, amber 60–79, green ≥ 80) on the TAM dashboard.
  • Score recomputed nightly; trend sparkline shown for 90 days.
  • Click-through to per-tenant detail page.

Story Points: 5


US-CUST-08-03 — Support-Ticket Cross-Reference

As a TAM, I want to see the tenant's support-ticket history in their portal admin view, So that I have full context on a single screen.

Acceptance Criteria:

  • Read-only widget pulls tickets from the support system (Zendesk/HelpScout adapter).
  • Last 10 tickets visible with status, priority, age.
  • Click-through opens the upstream ticket in a new tab.
  • Widget falls back gracefully if support adapter is unreachable.

Story Points: 3


US-CUST-08-04 — Tenant Annotations and Internal Notes

As a TAM, I want to leave private notes on a tenant record visible only to internal staff, So that context survives staff turnover.

Acceptance Criteria:

  • Notes editor on internal tenant view; markdown supported.
  • Notes table cust.tenant_notes (tenantId, authorId, body, createdAt, updatedAt).
  • Notes never returned by any tenant-facing API; RLS verified.
  • Edit/delete restricted to original author + platform admins.

Story Points: 3


EP-CUST-09 — Pashto/Dari/RTL Localisation Audit + Translation Memory

Goal: Ensure the portal renders correctly in Pashto and Dari (RTL), with managed translation memory so contributions can be reviewed and reused across releases.

US-CUST-09-01 — Locale-Switcher and Persistence

As a Pashto/Dari-speaking customer, I want to switch the portal locale, So that I can use the product in my language.

Acceptance Criteria:

  • Locale switcher in the navigation: en, fa-AF (Dari), ps (Pashto), ar.
  • Selection persisted in __locale cookie + user profile.
  • Server-side rendering uses the chosen locale on first paint (no flash of English).
  • HTML lang and dir attributes set per locale.

Story Points: 3


US-CUST-09-02 — RTL Layout Audit

As a RTL user, I want the entire UI to mirror correctly, So that controls, icons, and motion feel native.

Acceptance Criteria:

  • All Tailwind utilities use logical properties (ms-, me-, ps-, pe-) instead of ml-, mr-, pl-, pr-.
  • Icons that have inherent direction (chevrons, send-arrows) are mirrored under dir=rtl.
  • Charts (Recharts/Visx) honour RTL axis order where applicable.
  • Visual regression suite covers Pashto and Dari for all top-level pages at viewport widths 320, 768, 1280.

Story Points: 5


US-CUST-09-03 — ICU MessageFormat for Pluralisation and Gender

As a translator, I want message strings to use ICU MessageFormat, So that Pashto/Dari plural and gender variants render correctly.

Acceptance Criteria:

  • All user-visible strings extracted to locales/{locale}/messages.json in ICU MessageFormat.
  • Pluralisation tested with =0, =1, few, many, other for Pashto/Dari.
  • Gender selectors used where relevant (e.g., "{name} sent {gender, select, male{his} female{her} other{their}} message").
  • Lint rule blocks raw concatenation in user-visible code.

Story Points: 3


US-CUST-09-04 — Translation Memory Workflow (Crowdin or Lokalise)

As a localisation manager, I want translations managed in a TMS with review workflow, So that quality is consistent across releases and translators can reuse prior work.

Acceptance Criteria:

  • CI exports source strings to TMS on every merge to main.
  • CI imports approved translations on every release branch cut.
  • Glossary enforced for telecom terms (Pashto/Dari approved translations for: SMS, OTP, sender ID, delivery, compliance, hold, blocked, balance, invoice).
  • Translation coverage reported per release; release blocked if < 95% for fa-AF and ps.

Story Points: 3


Summary

EpicStoriesTotal Points
EP-CUST-01 Auth310
EP-CUST-02 API Keys311
EP-CUST-03 Test SMS15
EP-CUST-04 Message Logs211
EP-CUST-05 Webhooks18
EP-CUST-06 Billing15
EP-CUST-07 Session Security Hardening516
EP-CUST-08 Customer Success Surface416
EP-CUST-09 Pashto/Dari/RTL Localisation414
Total2496