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-serviceis 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 perEP-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
/loginrenders a "Sign in" CTA that initiates an OIDC Authorization Code flow with PKCE againstauth-service(/v1/auth/oidc/authorize). - PKCE: client generates
code_verifier(43–128 chars, RFC 7636) andcode_challenge = BASE64URL(SHA256(verifier));stateis a CSRF-resistant random nonce. - On callback at
/auth/callback, the code is exchanged for the platform JWT pair viaPOST /v1/auth/oidc/token;stateis verified. - Platform JWT (RS256, issued by
auth-service) is verified using the JWKS at/.well-known/jwks.json(cached 5 min). -
customerrole claim is verified; login rejected with explicit message if claim missing. - JWT and refresh token stored in
httpOnly; Secure; SameSite=Strictcookies (encrypted with the BFF session key). - User is redirected to
/dashboardon 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-Afterhonoured. - 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_ENABLEDdefaults tofalse; whentrue(legacy migration window), a secondary "Sign in with Firebase" CTA is shown — calls the legacyPOST /v1/auth/firebaseexchange viaauth-serviceFirebase legacy provider perEP-AUTH-05. - E2E test
tests/e2e/login-keycloak.spec.tscovers: happy path, missing role, expired state, replay-attack oncode, legacy Firebase fallback when flag is on. - Telemetry:
customer_portal_login_total{provider}counter;customer_portal_login_latency_secondshistogram; OTel spancustomer-portal.loginlinked to the upstreamauth-service.token-exchangespan.
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/refreshwith the__refreshcookie - 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/logoutclears__sessionand__refreshcookies - Firebase
signOut()is called client-side - User is redirected to
/loginafter 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-keyspage 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-keysis 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,
rawKeyis 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
revokedstatus 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-testpage has fields:to(E.164),from(Sender ID),body - Client-side validation: E.164 regex for
to, non-emptybody - Character counter shown; warning at > 160 chars (multi-part SMS)
- On submit,
POST /v1/messagescalled via BFF route - Success: show
messageIdwith 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:
-
/messagespage shows paginated table of messages - Columns: messageId, to, from, status badge, submittedAt, deliveredAt
- Filter panel: date range, status multi-select,
tofield,fromfield - 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:
-
/webhookspage 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:
-
/billingpage 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
overduebadge
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-Policyheader withdefault-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.jsheaders()and React Server Component context. - CSP report-only mirror header
Content-Security-Policy-Report-Onlyposts to/csp-reportfor one release before enforcement. - No
unsafe-inline, nounsafe-eval, no broadhttps:inscript-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-originupstream. - 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-portalpolicy that escapes toDOMPurifyfor any HTML sink. - Build step fails on
dangerouslySetInnerHTMLoutside 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
integrityfor every static asset reference. - CDN headers include
Cache-Control: public, max-age=31536000, immutableandCross-Origin-Resource-Policy: same-site. - CI gate fails if any
<script src>lacksintegrity. - 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-Q1returns 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.v1published 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
__localecookie + user profile. - Server-side rendering uses the chosen locale on first paint (no flash of English).
- HTML
langanddirattributes 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 ofml-,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.jsonin ICU MessageFormat. - Pluralisation tested with
=0,=1,few,many,otherfor 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%forfa-AFandps.
Story Points: 3
Summary
| Epic | Stories | Total Points |
|---|---|---|
| EP-CUST-01 Auth | 3 | 10 |
| EP-CUST-02 API Keys | 3 | 11 |
| EP-CUST-03 Test SMS | 1 | 5 |
| EP-CUST-04 Message Logs | 2 | 11 |
| EP-CUST-05 Webhooks | 1 | 8 |
| EP-CUST-06 Billing | 1 | 5 |
| EP-CUST-07 Session Security Hardening | 5 | 16 |
| EP-CUST-08 Customer Success Surface | 4 | 16 |
| EP-CUST-09 Pashto/Dari/RTL Localisation | 4 | 14 |
| Total | 24 | 96 |