Add the billing invoice list
Angular · Angular · advanced · modification
Adds the billing section. BillingService wraps the billing gateway's invoice endpoint, resolving it through the configured `apiOrigin` so the service targets the right gateway in every environment, and InvoiceListComponent renders the result with loading, error, and empty states. `auth.interceptor.ts` is shown unchanged for reviewer context.
The app is served from app.example.com; the proxy routes same-origin `/api/*` requests to the gateway, and `environment.apiOrigin` is `https://api.example.com` in every environment. The billing API treats a request without `Authorization`/`X-Org-Id` as the anonymous org and returns `200` with an empty invoice list rather than a `401`.
Requirements
- Add the billing section: signed-in users see their org's invoices (number, issue date, total, status) fetched from the billing API at /api/billing/invoices.
- Every first-party API call must go through authInterceptor, which attaches the org credentials (Authorization + X-Org-Id). A billing request that leaves without them is a defect even if the server answers 200.
- The interceptor's relative-URL condition is the documented security boundary: credentials must never be attached to absolute URLs, so the interceptor must not be widened.
- The invoice list handles loading, error, and empty states.
Files touched
- src/app/core/auth.interceptor.ts
- src/app/billing/billing.service.ts
- src/app/billing/invoice-list.component.ts
--- src/app/core/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { SessionService } from './session.service';
/**
* Attaches the signed-in org's credentials to first-party API calls.
*
* SECURITY BOUNDARY (docs/security.md): credentials are attached only to
* relative URLs under `/api/` — same-origin requests that the proxy routes
* to the gateway. Absolute URLs pass through untouched so the token and org
* id can never be sent to a third-party host. Do not widen this condition.
*/
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const session = inject(SessionService);
const token = session.token();
const orgId = session.orgId();
if (token && orgId && req.url.startsWith('/api/')) {
return next(
req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
'X-Org-Id': orgId,
},
}),
);
}
return next(req);
};