Files
halobestie-clone/requirement/phase1-api-contract.md
ramadhan sjamsani a7a2a32d27 Phase 1 scaffold: auth for all apps
- Backend: Fastify with two listeners (public + internal), routes, services, DB migration + seed
- client_app: Flutter with BLoC, all auth screens (welcome, display name, register, OTP, force-register)
- mitra_app: Flutter with BLoC, OTP-only login
- control_center: React + Vite, email/password login, mitra/user management, anonymity settings
- Docs: phase1 plan, API contract, client app mockup
- CLAUDE.md and shared memory for all subprojects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:08:42 +08:00

453 lines
7.8 KiB
Markdown

# Phase 1 — API Contract
## General Conventions
- Base URL public: `https://api.halobestie.com`
- Base URL internal: `https://internal.halobestie.com`
- All requests: `Content-Type: application/json`
- All authenticated requests: `Authorization: Bearer <firebase_jwt>`
- All responses follow this envelope:
```json
// Success
{
"success": true,
"data": { ... }
}
// Error
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human readable message"
}
}
```
---
## Error Codes
| Code | HTTP | Description |
|---|---|---|
| `UNAUTHORIZED` | 401 | Missing or invalid Firebase JWT |
| `FORBIDDEN` | 403 | Valid token but insufficient role/permission |
| `NOT_FOUND` | 404 | Resource not found |
| `VALIDATION_ERROR` | 422 | Request body/params failed validation |
| `ACCOUNT_NOT_FOUND` | 404 | Phone number not registered as mitra |
| `ACCOUNT_INACTIVE` | 403 | Mitra account is inactive |
| `DISPLAY_NAME_REQUIRED` | 422 | Display name not provided |
| `ALREADY_REGISTERED` | 409 | Anonymous user already has phone/social linked |
| `INTERNAL_ERROR` | 500 | Unexpected server error |
---
## Public API (port 3000)
### Customer
#### `POST /api/shared/customer/anonymous`
Create an anonymous customer with a display name.
**Auth:** None
**Request:**
```json
{
"display_name": "Angin Malam"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": true,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `DISPLAY_NAME_REQUIRED`, `VALIDATION_ERROR`
---
#### `POST /api/shared/customer/link`
Link phone OTP or social login to an existing anonymous customer.
**Auth:** Firebase JWT (anonymous customer)
**Request:**
```json
{
"customer_id": "uuid",
"firebase_uid": "firebase_uid_from_auth"
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": false,
"phone": "+628xxxxxxxxxx",
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `NOT_FOUND`, `ALREADY_REGISTERED`
---
#### `POST /api/client/auth/verify`
Verify Firebase JWT and return customer profile. Called after every login.
**Auth:** Firebase JWT (customer)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Angin Malam",
"is_anonymous": false,
"phone": "+628xxxxxxxxxx",
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `NOT_FOUND`
---
### Mitra
#### `POST /api/mitra/auth/verify`
Verify Firebase JWT and return mitra profile. Called after OTP login.
**Auth:** Firebase JWT (mitra)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"display_name": "Dr. Budi",
"phone": "+628xxxxxxxxxx",
"is_active": true,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `ACCOUNT_NOT_FOUND`, `ACCOUNT_INACTIVE`
---
### Anonymity Config (read-only for apps)
#### `GET /api/shared/config/anonymity`
Get current anonymity setting. Apps poll this to decide whether to show force-register wall.
**Auth:** Firebase JWT (customer)
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
```
---
## Internal API (port 3001)
> All internal routes require a valid Firebase JWT with `role` verified server-side.
### Auth
#### `POST /internal/auth/verify`
Verify Firebase JWT and return control center user profile with role and permissions.
**Auth:** Firebase JWT (control center user)
**Request:** _(empty body)_
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"email": "admin@halobestie.com",
"display_name": "Admin",
"role": {
"id": "uuid",
"name": "super_admin",
"permissions": {
"mitra": ["create", "read", "update", "delete"],
"control_center_users": ["create", "read", "update", "delete"],
"config": ["read", "update"]
}
}
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`
---
### Mitra Management
#### `POST /internal/mitras`
Create a new mitra record.
**Auth:** Firebase JWT (control center user, requires `mitra:create` permission)
**Request:**
```json
{
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi",
"is_active": false,
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`
---
#### `GET /internal/mitras`
List all mitras.
**Auth:** Firebase JWT (control center user, requires `mitra:read` permission)
**Query params:**
- `page` (default: 1)
- `limit` (default: 20)
- `is_active` (optional: `true` | `false`)
**Response `200`:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"phone": "+628xxxxxxxxxx",
"display_name": "Dr. Budi",
"is_active": true,
"created_at": "2026-04-04T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
}
```
---
#### `PATCH /internal/mitras/:id/status`
Activate or deactivate a mitra.
**Auth:** Firebase JWT (control center user, requires `mitra:update` permission)
**Request:**
```json
{
"is_active": true
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"is_active": true
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `VALIDATION_ERROR`
---
### Control Center User Management
#### `POST /internal/control-center-users`
Create a new control center user.
**Auth:** Firebase JWT (requires `control_center_users:create` permission)
**Request:**
```json
{
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role_id": "uuid"
}
```
**Response `201`:**
```json
{
"success": true,
"data": {
"id": "uuid",
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role": {
"id": "uuid",
"name": "operator"
},
"created_at": "2026-04-04T00:00:00Z"
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`
---
#### `GET /internal/control-center-users`
List all control center users.
**Auth:** Firebase JWT (requires `control_center_users:read` permission)
**Query params:**
- `page` (default: 1)
- `limit` (default: 20)
**Response `200`:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"email": "operator@halobestie.com",
"display_name": "Operator 1",
"role": {
"id": "uuid",
"name": "operator"
},
"created_at": "2026-04-04T00:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}
}
```
---
### Roles
#### `GET /internal/roles`
List all roles.
**Auth:** Firebase JWT (requires `control_center_users:read` permission)
**Response `200`:**
```json
{
"success": true,
"data": [
{
"id": "uuid",
"name": "super_admin",
"permissions": {
"mitra": ["create", "read", "update", "delete"],
"control_center_users": ["create", "read", "update", "delete"],
"config": ["read", "update"]
}
}
]
}
```
---
### Config
#### `GET /internal/config/anonymity`
Get anonymity setting.
**Auth:** Firebase JWT (requires `config:read` permission)
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": true
}
}
```
---
#### `PATCH /internal/config/anonymity`
Toggle anonymity setting.
**Auth:** Firebase JWT (requires `config:update` permission)
**Request:**
```json
{
"anonymity_enabled": false
}
```
**Response `200`:**
```json
{
"success": true,
"data": {
"anonymity_enabled": false
}
}
```
**Errors:** `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`