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>
This commit is contained in:
452
requirement/phase1-api-contract.md
Normal file
452
requirement/phase1-api-contract.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user