|
| 1 | +# API Integration Guide |
| 2 | + |
| 3 | +This document describes how the BettaPay frontend talks to the backend API, what endpoints the current UI expects, and how to test API-dependent flows locally or in preview deployments. |
| 4 | + |
| 5 | +## API client configuration |
| 6 | + |
| 7 | +The shared Axios client lives in [`lib/api/axios.ts`](../lib/api/axios.ts): |
| 8 | + |
| 9 | +```ts |
| 10 | +export const apiClient = axios.create({ |
| 11 | + baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001', |
| 12 | + headers: { 'Content-Type': 'application/json' }, |
| 13 | + withCredentials: true, |
| 14 | +}); |
| 15 | +``` |
| 16 | + |
| 17 | +Key behavior: |
| 18 | + |
| 19 | +- `NEXT_PUBLIC_API_URL` controls the backend base URL. |
| 20 | +- If the variable is unset, the frontend defaults to `http://localhost:3001`. |
| 21 | +- `withCredentials: true` is enabled so browser requests include backend-set cookies. |
| 22 | +- JSON is the default request/response format. |
| 23 | +- State-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`) attach the CSRF header from the CSRF cookie when available. |
| 24 | +- A separate refresh client posts to `/api/auth/refresh` after a `401` response, then retries the original request. |
| 25 | +- A `429` response updates the rate-limit store and shows an accessible toast/announcement. |
| 26 | + |
| 27 | +## Authentication model |
| 28 | + |
| 29 | +The frontend is designed around cookie-based authentication: |
| 30 | + |
| 31 | +- The backend should set auth/session tokens in `HttpOnly` cookies. |
| 32 | +- The client-side Zustand auth store may hold an in-memory token for the active tab, but it must not persist the token to `localStorage`. |
| 33 | +- The auth store persists only the non-sensitive `role` field. |
| 34 | +- Logout calls `DELETE /api/auth/session` so the backend can clear the auth cookie. |
| 35 | + |
| 36 | +See also: |
| 37 | + |
| 38 | +- [`docs/adr/001-state-management.md`](adr/001-state-management.md) |
| 39 | +- [`docs/adr/004-mock-auth.md`](adr/004-mock-auth.md) |
| 40 | + |
| 41 | +## CSRF and CORS requirements |
| 42 | + |
| 43 | +For local development and hosted environments, the backend must allow the frontend origin and credentials. |
| 44 | + |
| 45 | +Backend CORS should allow: |
| 46 | + |
| 47 | +- The local frontend origin, usually `http://localhost:3000`. |
| 48 | +- Vercel preview/production origins used by the project. |
| 49 | +- Credentialed requests (`Access-Control-Allow-Credentials: true`). |
| 50 | +- JSON request headers. |
| 51 | +- The CSRF header name used by [`lib/utils/csrf.ts`](../lib/utils/csrf.ts). |
| 52 | + |
| 53 | +For state-changing endpoints, the backend should validate the CSRF token using the same double-submit cookie pattern expected by the frontend. |
| 54 | + |
| 55 | +## Expected endpoints |
| 56 | + |
| 57 | +### `POST /api/auth/session` |
| 58 | + |
| 59 | +Sets an authentication cookie for the browser session. |
| 60 | + |
| 61 | +Used by: |
| 62 | + |
| 63 | +- Email login in [`app/auth/login/page.tsx`](../app/auth/login/page.tsx) |
| 64 | +- Wallet login in [`app/auth/login/page.tsx`](../app/auth/login/page.tsx) |
| 65 | + |
| 66 | +Request body: |
| 67 | + |
| 68 | +```json |
| 69 | +{ |
| 70 | + "token": "mock_jwt_token_12345", |
| 71 | + "role": "merchant" |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +Expected response: |
| 76 | + |
| 77 | +```json |
| 78 | +{ |
| 79 | + "ok": true |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Backend responsibilities: |
| 84 | + |
| 85 | +- Validate the token or session payload in real deployments. |
| 86 | +- Set an `HttpOnly`, `Secure` where appropriate, `SameSite` auth cookie. |
| 87 | +- Return a non-2xx status for invalid session data. |
| 88 | + |
| 89 | +### `GET /api/auth/session` |
| 90 | + |
| 91 | +Restores a session when the in-memory auth store was lost but a persisted role exists. |
| 92 | + |
| 93 | +Used by: |
| 94 | + |
| 95 | +- [`lib/hooks/useSessionCheck.ts`](../lib/hooks/useSessionCheck.ts) |
| 96 | + |
| 97 | +Expected successful response: |
| 98 | + |
| 99 | +```json |
| 100 | +{ |
| 101 | + "user": { |
| 102 | + "id": "merchant-id", |
| 103 | + "email": "merchant@example.com", |
| 104 | + "name": "Merchant User", |
| 105 | + "role": "merchant" |
| 106 | + }, |
| 107 | + "token": "session-token" |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +If the backend returns `200` without `user` and `token`, the hook treats the cookie-backed session as valid and leaves client state alone. |
| 112 | + |
| 113 | +Expected expired/invalid response: |
| 114 | + |
| 115 | +```json |
| 116 | +{ |
| 117 | + "error": "Session expired" |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +Use a `401` or `403` status for expired/invalid sessions so the frontend clears auth state and redirects to login. |
| 122 | + |
| 123 | +### `DELETE /api/auth/session` |
| 124 | + |
| 125 | +Clears the auth cookie during logout. |
| 126 | + |
| 127 | +Used by: |
| 128 | + |
| 129 | +- [`lib/store/authStore.ts`](../lib/store/authStore.ts) |
| 130 | + |
| 131 | +Expected response: |
| 132 | + |
| 133 | +```json |
| 134 | +{ |
| 135 | + "ok": true |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +Backend responsibilities: |
| 140 | + |
| 141 | +- Clear the same cookie name used by `POST /api/auth/session`. |
| 142 | +- Keep the endpoint idempotent; repeated logout requests should still succeed. |
| 143 | + |
| 144 | +### `POST /api/auth/refresh` |
| 145 | + |
| 146 | +Refreshes a cookie-backed session after an API request receives `401`. |
| 147 | + |
| 148 | +Used by: |
| 149 | + |
| 150 | +- The Axios response interceptor in [`lib/api/axios.ts`](../lib/api/axios.ts) |
| 151 | + |
| 152 | +Expected response: |
| 153 | + |
| 154 | +```json |
| 155 | +{ |
| 156 | + "ok": true |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +When this call succeeds, the frontend retries the original request. When it fails, the frontend logs the user out and redirects to `/auth/login`. |
| 161 | + |
| 162 | +### `GET /api/merchants/:id` |
| 163 | + |
| 164 | +Fetches merchant profile data for the login flow. |
| 165 | + |
| 166 | +Used by: |
| 167 | + |
| 168 | +- Email login in [`app/auth/login/page.tsx`](../app/auth/login/page.tsx) |
| 169 | + |
| 170 | +Expected successful response: |
| 171 | + |
| 172 | +```json |
| 173 | +{ |
| 174 | + "id": "GCCHHKNI7GRA5QWC7RCTT3OHO7SKAUMKQA6IBWEQEO2SXI3GF376UHDD", |
| 175 | + "name": "Merchant User" |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +If the backend is unavailable or returns an error-like response, the login page falls back to mock merchant data for local/preview usability. |
| 180 | + |
| 181 | +### `POST /api/merchants` |
| 182 | + |
| 183 | +Registers a merchant record. |
| 184 | + |
| 185 | +Used by: |
| 186 | + |
| 187 | +- [`app/auth/register/page.tsx`](../app/auth/register/page.tsx) |
| 188 | + |
| 189 | +Request body currently sent by the frontend: |
| 190 | + |
| 191 | +```json |
| 192 | +{ |
| 193 | + "id": "merch_generatedid", |
| 194 | + "name": "Acme Corp" |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +Expected response: |
| 199 | + |
| 200 | +```json |
| 201 | +{ |
| 202 | + "id": "merch_generatedid", |
| 203 | + "name": "Acme Corp" |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +If this call fails in preview/local mock mode, the registration page shows a successful mock registration after a short delay. |
| 208 | + |
| 209 | +### `POST /api/payments` |
| 210 | + |
| 211 | +Creates or submits a payment for a payment link. |
| 212 | + |
| 213 | +Used by: |
| 214 | + |
| 215 | +- [`app/pay/[linkId]/page.tsx`](../app/pay/%5BlinkId%5D/page.tsx) |
| 216 | + |
| 217 | +The exact request shape should stay aligned with the payment form fields and backend payment contract. At minimum, the backend should return a payment identifier or transaction metadata that the UI can display after submission. |
| 218 | + |
| 219 | +### `GET /api/payments/:txId` |
| 220 | + |
| 221 | +Fetches payment status by transaction id. |
| 222 | + |
| 223 | +Used by: |
| 224 | + |
| 225 | +- [`app/pay/status/[txId]/page.tsx`](../app/pay/status/%5BtxId%5D/page.tsx) |
| 226 | + |
| 227 | +Expected response should include the payment/transaction status, amount, asset, and any display metadata needed by the status page. |
| 228 | + |
| 229 | +## Local backend development |
| 230 | + |
| 231 | +Run the backend on port `3001` or set the frontend to the backend URL: |
| 232 | + |
| 233 | +```bash |
| 234 | +NEXT_PUBLIC_API_URL=http://localhost:3001 |
| 235 | +``` |
| 236 | + |
| 237 | +Then start the frontend: |
| 238 | + |
| 239 | +```bash |
| 240 | +npm run dev |
| 241 | +``` |
| 242 | + |
| 243 | +The frontend runs at `http://localhost:3000` by default. The backend must allow that origin with credentials. |
| 244 | + |
| 245 | +## Vercel preview and mock mode |
| 246 | + |
| 247 | +Preview deployments may not have a live backend. The auth flow deliberately falls back to mock behavior so reviewers can navigate the UI: |
| 248 | + |
| 249 | +- Login accepts any email. |
| 250 | +- Emails containing `admin` receive the `admin` role; others receive `merchant`. |
| 251 | +- The mock token is kept in memory only. |
| 252 | +- The app attempts to set the session cookie, but continues when `/api/auth/session` is unavailable. |
| 253 | +- Merchant fetch and registration calls fall back to mock data when the backend is unavailable. |
| 254 | + |
| 255 | +This behavior is for development and previews only. It is not a production authentication system. |
| 256 | + |
| 257 | +## Testing endpoints with curl |
| 258 | + |
| 259 | +Replace `http://localhost:3001` with your backend URL. |
| 260 | + |
| 261 | +Set auth cookie: |
| 262 | + |
| 263 | +```bash |
| 264 | +curl -i -X POST http://localhost:3001/api/auth/session \ |
| 265 | + -H 'Content-Type: application/json' \ |
| 266 | + --data '{"token":"mock_jwt_token_12345","role":"merchant"}' |
| 267 | +``` |
| 268 | + |
| 269 | +Check session: |
| 270 | + |
| 271 | +```bash |
| 272 | +curl -i http://localhost:3001/api/auth/session \ |
| 273 | + --cookie 'auth_token=example' |
| 274 | +``` |
| 275 | + |
| 276 | +Clear session: |
| 277 | + |
| 278 | +```bash |
| 279 | +curl -i -X DELETE http://localhost:3001/api/auth/session \ |
| 280 | + --cookie 'auth_token=example' |
| 281 | +``` |
| 282 | + |
| 283 | +Fetch merchant: |
| 284 | + |
| 285 | +```bash |
| 286 | +curl -i http://localhost:3001/api/merchants/GCCHHKNI7GRA5QWC7RCTT3OHO7SKAUMKQA6IBWEQEO2SXI3GF376UHDD |
| 287 | +``` |
| 288 | + |
| 289 | +Create merchant: |
| 290 | + |
| 291 | +```bash |
| 292 | +curl -i -X POST http://localhost:3001/api/merchants \ |
| 293 | + -H 'Content-Type: application/json' \ |
| 294 | + --data '{"id":"merch_demo","name":"Demo Merchant"}' |
| 295 | +``` |
| 296 | + |
| 297 | +## Troubleshooting |
| 298 | + |
| 299 | +- **Cookies are not sent:** confirm `withCredentials: true`, backend CORS `Access-Control-Allow-Credentials: true`, and a matching allowed origin. |
| 300 | +- **CSRF failures:** confirm the CSRF cookie exists and the backend expects the same CSRF header name as the frontend. |
| 301 | +- **Login works in preview but not locally:** verify `NEXT_PUBLIC_API_URL` points to a running backend and that CORS allows `http://localhost:3000`. |
| 302 | +- **User is redirected after reload:** the persisted role exists, but `GET /api/auth/session` may be returning a non-2xx response; inspect the network tab and backend logs. |
0 commit comments