init push
This commit is contained in:
423
docs/API_GUIDE.md
Normal file
423
docs/API_GUIDE.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# API Guide STEP 1 & STEP 2
|
||||
|
||||
Dokumentasi ringkas untuk endpoint yang paling sering dipakai di backend ini.
|
||||
|
||||
## Helicopter APIs
|
||||
|
||||
### 1. Create helicopter
|
||||
- `POST /api/v1/helicopters/create`
|
||||
- Fungsi: membuat data helicopter baru.
|
||||
- Cara pakai: kirim payload create helicopter sesuai schema di Swagger.
|
||||
|
||||
### 2. Update helicopter
|
||||
- `PATCH /api/v1/helicopters/update/{id}`
|
||||
- Fungsi: update sebagian field helicopter berdasarkan `id`.
|
||||
- Cara pakai: kirim field yang ingin diubah saja.
|
||||
|
||||
### 3. Delete helicopter
|
||||
- `DELETE /api/v1/helicopters/delete/{id}`
|
||||
- Fungsi: soft delete helicopter.
|
||||
- Cara pakai: kirim `id` helicopter di path.
|
||||
|
||||
### 4. Generate next report number
|
||||
- `POST /api/v1/helicopters/{id}/reports/next-number`
|
||||
- Fungsi: generate nomor report berikutnya dengan format `<identifier>-NNNN`.
|
||||
- Cara pakai: kirim `id` helicopter di path.
|
||||
|
||||
### 5. Get helicopter by ID
|
||||
- `GET /api/v1/helicopters/get/{id}`
|
||||
- Fungsi: ambil detail helicopter berdasarkan `id`.
|
||||
- Cara pakai: kirim `id` helicopter di path.
|
||||
|
||||
### 6. Get all helicopters
|
||||
- `GET /api/v1/helicopters/get-all`
|
||||
- Fungsi: list helicopter dengan filter, sort, dan pagination.
|
||||
- Cara pakai:
|
||||
- `search` atau `filter[search]`
|
||||
- `sort`
|
||||
- `page[number]`, `page[size]`
|
||||
- `page`, `size`, `limit`
|
||||
|
||||
### 7. Get all helicopters (datatable)
|
||||
- `GET /api/v1/helicopters/get-all/dt`
|
||||
- Fungsi: list helicopter untuk datatable.
|
||||
- Cara pakai: gunakan parameter pagination datatable yang didukung backend.
|
||||
|
||||
## Bases APIs
|
||||
|
||||
### 1. Create base
|
||||
- `POST /api/v1/bases/create`
|
||||
- Fungsi: membuat base baru.
|
||||
- Cara pakai: kirim payload base create sesuai Swagger.
|
||||
|
||||
### 2. Update base
|
||||
- `PATCH /api/v1/bases/update/{uuid}`
|
||||
- Fungsi: update sebagian field base.
|
||||
- Cara pakai: kirim `uuid` base di path, lalu field yang ingin diubah.
|
||||
|
||||
### 3. Delete base
|
||||
- `DELETE /api/v1/bases/delete/{uuid}`
|
||||
- Fungsi: soft delete base.
|
||||
- Cara pakai: kirim `uuid` base di path.
|
||||
|
||||
### 4. Get base by ID
|
||||
- `GET /api/v1/bases/get/{uuid}`
|
||||
- Fungsi: ambil detail base.
|
||||
- Cara pakai: kirim `uuid` base di path.
|
||||
|
||||
### 5. Get all bases
|
||||
- `GET /api/v1/bases/get-all`
|
||||
- Fungsi: list base dengan filter, sort, dan pagination.
|
||||
- Cara pakai:
|
||||
- `search` atau `filter[search]`
|
||||
- `sort`
|
||||
- `page[number]`, `page[size]`
|
||||
- `page`, `size`, `limit`
|
||||
|
||||
### 6. Get all bases (datatable)
|
||||
- `GET /api/v1/bases/get-all/dt`
|
||||
- Fungsi: list base untuk datatable.
|
||||
- Cara pakai: gunakan parameter pagination datatable yang didukung backend.
|
||||
|
||||
## Contact APIs
|
||||
|
||||
### 1. Create contact
|
||||
- `POST /api/v1/contacts/create`
|
||||
- Fungsi: membuat user contact baru beserta profile role-specific.
|
||||
- Cara pakai:
|
||||
- `data.type` harus `contact_create`
|
||||
- `role_ids` menentukan role
|
||||
- setiap role yang dipilih harus punya object profile yang sesuai di `attributes`
|
||||
|
||||
### 2. Update contact
|
||||
- `PATCH /api/v1/contacts/{user_id}`
|
||||
- Fungsi: update contact.
|
||||
- Cara pakai:
|
||||
- `data.type` harus `contact_update`
|
||||
- bisa update `role_id` dan `role_ids`
|
||||
- `role_ids: []` akan menghapus semua role
|
||||
- hanya satu profile role-specific boleh ada di `attributes`
|
||||
|
||||
### 3. Get contact
|
||||
- `GET /api/v1/contacts/{user_id}`
|
||||
- Fungsi: ambil detail contact.
|
||||
- Cara pakai: kirim `user_id` di path.
|
||||
|
||||
### 4. Get all contacts
|
||||
- `GET /api/v1/contacts/get-all`
|
||||
- Fungsi: list contact dengan filter role, pilot category, sort, dan pagination.
|
||||
- Cara pakai:
|
||||
- filter role / pilot category sesuai kebutuhan frontend
|
||||
- sort field seperti `name`, `email`, `is_active`, `created_at`, `updated_at`, dan lainnya
|
||||
|
||||
### 5. Get all contacts (datatable)
|
||||
- `GET /api/v1/contacts/get-all/dt`
|
||||
- Fungsi: list contact untuk datatable.
|
||||
- Cara pakai: gunakan parameter pagination datatable yang didukung backend.
|
||||
|
||||
### 6. Delete contact
|
||||
- `DELETE /api/v1/contacts/{user_id}`
|
||||
- Fungsi: soft delete contact.
|
||||
- Cara pakai: kirim `user_id` di path.
|
||||
|
||||
### 7. Update contact active status
|
||||
- `PATCH /api/v1/contacts/{user_id}/status`
|
||||
- Fungsi: update status aktif contact.
|
||||
- Cara pakai: kirim `user_id` di path dan payload status.
|
||||
|
||||
### 8. Update contact role
|
||||
- `PATCH /api/v1/contacts/{user_id}/role`
|
||||
- Fungsi: update role contact.
|
||||
- Cara pakai: kirim `user_id` di path dan payload role baru.
|
||||
|
||||
### 9. Update pilot category
|
||||
- `PATCH /api/v1/contacts/{user_id}/pilot-category`
|
||||
- Fungsi: update pilot category.
|
||||
- Cara pakai: kirim `user_id` di path dan payload kategori.
|
||||
|
||||
### 10. Update contact license fields
|
||||
- `PATCH /api/v1/contacts/{user_id}/license`
|
||||
- Fungsi: update data lisensi contact.
|
||||
- Cara pakai: kirim `user_id` di path dan payload field lisensi.
|
||||
|
||||
### 11. Update chief flags
|
||||
- `PATCH /api/v1/contacts/{user_id}/chief-flags`
|
||||
- Fungsi: update flag chief.
|
||||
- Cara pakai: kirim `user_id` di path dan payload flag yang ingin diubah.
|
||||
|
||||
### 12. Update responsible complaints flag
|
||||
- `PATCH /api/v1/contacts/{user_id}/responsible-complaints`
|
||||
- Fungsi: update flag responsible complaints.
|
||||
- Cara pakai: kirim `user_id` di path dan payload flag yang ingin diubah.
|
||||
|
||||
### 13. Bulk update contacts status
|
||||
- `POST /api/v1/contacts/bulk-update`
|
||||
- Fungsi: update status banyak contact sekaligus.
|
||||
- Cara pakai: kirim daftar contact yang ingin diupdate.
|
||||
|
||||
## Flights API
|
||||
|
||||
### Get all flights
|
||||
- `GET /api/v1/flights/get-all`
|
||||
- Fungsi: list flight dengan pagination, filter, sort, dan step workflow.
|
||||
- Cara pakai:
|
||||
- `search` atau `filter[search]`
|
||||
- `date` atau `filter[date]` untuk filter tanggal flight
|
||||
- `sort`
|
||||
- `page[number]`, `page[size]`
|
||||
- `page`, `size`, `limit`
|
||||
|
||||
### Contoh response
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "flight",
|
||||
"id": "019e8afa-e602-716c-8d2a-b6b11a89caf0",
|
||||
"attributes": {
|
||||
"status": "",
|
||||
"mission_code": "M-060326-001",
|
||||
"date": "2026-06-03",
|
||||
"total_draft": 2,
|
||||
"takeover": {
|
||||
"id": "019e8afa-e5fe-7de0-ac5d-fb5ab95896b0",
|
||||
"daily_inspection": {
|
||||
"name": "test test",
|
||||
"short_name": "",
|
||||
"date": "05.06.2026",
|
||||
"utc_time": "05:03"
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"phase": "takeover",
|
||||
"position": "takeover",
|
||||
"step": 1,
|
||||
"complete": true,
|
||||
"id": "019e8afa-e5fe-7de0-ac5d-fb5ab95896b0",
|
||||
"status": "completed",
|
||||
"completed_at": "2026-06-03T00:55:59Z",
|
||||
"completed_by": "test test"
|
||||
},
|
||||
{
|
||||
"phase": "mission",
|
||||
"step": 2,
|
||||
"complete": true,
|
||||
"status": "assigned",
|
||||
"mission_type": [
|
||||
{
|
||||
"type": "CAT",
|
||||
"draft": 2,
|
||||
"completed": 0
|
||||
}
|
||||
],
|
||||
"completed_at": "",
|
||||
"completed_by": ""
|
||||
}
|
||||
],
|
||||
"draft": [
|
||||
{
|
||||
"type": "CAT",
|
||||
"items": [
|
||||
{
|
||||
"id": "019e9640-f7af-7940-80e8-1e9a83013afb",
|
||||
"type": "CAT",
|
||||
"status": "draft",
|
||||
"flight_data_id": "019e9640-f7b0-7787-b43c-0b395ff2792a"
|
||||
},
|
||||
{
|
||||
"id": "019e9640-f8cf-72b1-84dc-b50c1a3c2cc6",
|
||||
"type": "CAT",
|
||||
"status": "draft",
|
||||
"flight_data_id": "019e9640-f8d0-7ea2-b250-a7e31bfba95f"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"created_at": "2026-06-03T00:55:59Z",
|
||||
"created_by": "test test",
|
||||
"updated_at": "2026-06-03T00:55:59Z",
|
||||
"updated_by": "test test"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"page_number": 1,
|
||||
"page_size": 20,
|
||||
"total": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Takeover APIs
|
||||
|
||||
### 1. Create takeover
|
||||
- `POST /api/v1/takeovers/create`
|
||||
- Fungsi: membuat takeover secara atomik.
|
||||
- Cara pakai:
|
||||
- `base_id`, `duty_date`, `helicopter_id`
|
||||
- `inspection`
|
||||
- `roster_detail`
|
||||
- `notes`
|
||||
- Catatan:
|
||||
- `shift_date` dan `shift_time` sudah tidak dipakai lagi di takeover request.
|
||||
- shift diambil dari `base`.
|
||||
|
||||
### 2. Update takeover
|
||||
- `PATCH /api/v1/takeovers/update/{id}`
|
||||
- Fungsi: replace takeover berdasarkan flight UUID.
|
||||
- Cara pakai:
|
||||
- kirim `id` takeover / flight di path
|
||||
- body mengikuti format takeover update yang sama dengan create
|
||||
|
||||
### 3. Get takeover by ID
|
||||
- `GET /api/v1/takeovers/get/{id}`
|
||||
- Fungsi: ambil takeover detail.
|
||||
- Cara pakai: kirim `id` di path.
|
||||
|
||||
### 4. Get all takeovers
|
||||
- `GET /api/v1/takeovers/get-all`
|
||||
- Fungsi: list takeover composite yang dibangun dari flight, reserve AC, duty roster, dan inspection.
|
||||
- Cara pakai: tidak ada parameter wajib.
|
||||
|
||||
### 5. Delete takeover
|
||||
- `DELETE /api/v1/takeovers/delete/{id}`
|
||||
- Fungsi: delete takeover berdasarkan flight UUID.
|
||||
- Cara pakai: kirim `id` di path.
|
||||
|
||||
### Payload create/update takeover
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"attributes": {
|
||||
"base_id": "string",
|
||||
"duty_date": "string",
|
||||
"helicopter_id": "string",
|
||||
"inspection": {
|
||||
"before": {
|
||||
"file_checklist": [
|
||||
{
|
||||
"helicopter_file_id": "019d7000-aaaa-7bbb-8ccc-111111111111",
|
||||
"is_done": true
|
||||
}
|
||||
],
|
||||
"fuel_added_amount": 0,
|
||||
"fuel_amount": 0,
|
||||
"fuel_unit": "string",
|
||||
"hydraulic_lh_checked": true,
|
||||
"hydraulic_rh_checked": true,
|
||||
"note": "string",
|
||||
"oil_engine_nr1_checked": true,
|
||||
"oil_engine_nr2_checked": true,
|
||||
"oil_transmission_igb_checked": true,
|
||||
"oil_transmission_mgb_checked": true,
|
||||
"oil_transmission_tgb_checked": true
|
||||
},
|
||||
"inspection_date": "2026-04-15",
|
||||
"prepare": {
|
||||
"file_checklist": [
|
||||
{
|
||||
"helicopter_file_id": "019d7000-aaaa-7bbb-8ccc-111111111111",
|
||||
"is_done": true
|
||||
}
|
||||
],
|
||||
"notam_briefing": true,
|
||||
"operational_flight_plan": true,
|
||||
"weather_briefing": true
|
||||
}
|
||||
},
|
||||
"notes": "string",
|
||||
"roster_detail": {
|
||||
"doctor": [
|
||||
{
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"
|
||||
}
|
||||
],
|
||||
"helper": [
|
||||
{
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"
|
||||
}
|
||||
],
|
||||
"other_person": [
|
||||
{
|
||||
"email": "guest@example.com",
|
||||
"mobile_phone": "+43-1234",
|
||||
"name": "Guest Person"
|
||||
}
|
||||
],
|
||||
"pilot": [
|
||||
{
|
||||
"co_pilot": true,
|
||||
"crew_type": "main",
|
||||
"examiner": true,
|
||||
"flight_instructor": true,
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d",
|
||||
"line_checker": true,
|
||||
"supervisor": true
|
||||
}
|
||||
],
|
||||
"rescuer": [
|
||||
{
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "takeover_create"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response takeover detail
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover",
|
||||
"id": "019e8713-aa19-7fb7-a510-a0332dc69697",
|
||||
"attributes": {
|
||||
"id": "019e8713-aa19-7fb7-a510-a0332dc69697",
|
||||
"flight_id": "019e8afa-e602-716c-8d2a-b6b11a89caf0",
|
||||
"reserve_ac_id": "019e8713-aa19-7fb7-a510-a0332dc69698",
|
||||
"flight_inspection_id": "019e8713-aa19-7fb7-a510-a0332dc69699",
|
||||
"shift_start": "05:00:00",
|
||||
"shift_end": "13:00:00",
|
||||
"base_id": "019d61db-53c6-7280-87ec-e65f205c6d3b",
|
||||
"base": {
|
||||
"id": "019d61db-53c6-7280-87ec-e65f205c6d3b",
|
||||
"name": "Base Name"
|
||||
},
|
||||
"duty_date": "2026-06-03",
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"helicopter": {
|
||||
"id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"identifier": "D-HYAR",
|
||||
"designation": "AIRBUS H145",
|
||||
"type": "H145",
|
||||
"aog": false,
|
||||
"is_active": true
|
||||
},
|
||||
"notes": "string",
|
||||
"roster_detail": {
|
||||
"pilot": [],
|
||||
"doctor": [],
|
||||
"rescuer": [],
|
||||
"helper": [],
|
||||
"other_person": []
|
||||
},
|
||||
"inspection": {
|
||||
"before": {},
|
||||
"prepare": {},
|
||||
"inspection_date": "2026-06-03"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `takeover` request tidak memakai `shift_date` dan `shift_time`.
|
||||
- `flights/get-all` menampilkan step `takeover`, `mission`, dan `after_flight_inspection` kalau record after inspection sudah ada.
|
||||
- `mission.step` ditampilkan sebagai `step: 2`.
|
||||
- `complete` selalu ada di response step; kalau belum selesai nilainya `false`.
|
||||
- `mission_type.completed` masih angka, karena itu hitungan draft/completed, bukan boolean.
|
||||
96
docs/AUTH_SSO_FLOW.md
Normal file
96
docs/AUTH_SSO_FLOW.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# AUTH SSO FLOW (Frontend)
|
||||
|
||||
## 1) New SSO Login Flow
|
||||
1. Frontend calls `GET /api/v1/auth/microsoft/login`.
|
||||
2. Redirect user to `data.attributes.url`.
|
||||
3. Provider redirects to backend callback `GET /api/v1/auth/microsoft/callback?code=...&state=...`.
|
||||
4. Backend checks existing SSO mapping (`provider=microsoft` + `provider_subject`).
|
||||
5. Result:
|
||||
- Mapping + user exists: login success, cookies/session issued.
|
||||
- Mapping not found: login rejected (`401`).
|
||||
- Mapping exists but user missing: login rejected (`404`).
|
||||
|
||||
## 2) Important Rule
|
||||
SSO **does not create new account automatically**. User must already exist and be linked first.
|
||||
|
||||
## 3) Link SSO (for logged-in existing user)
|
||||
Endpoint: `POST /api/v1/auth/sso/link`
|
||||
|
||||
Auth: required (same auth cookie/session as normal login).
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_link",
|
||||
"attributes": {
|
||||
"provider": "microsoft",
|
||||
"code": "<microsoft_authorization_code>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success `200`:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_link",
|
||||
"attributes": {
|
||||
"provider": "microsoft",
|
||||
"provider_subject": "00000000-0000-0000-0000-000000000000",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4) Unlink SSO
|
||||
Endpoint: `DELETE /api/v1/auth/sso/unlink`
|
||||
|
||||
Auth: required.
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_unlink",
|
||||
"attributes": {
|
||||
"provider": "microsoft"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success `200`:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_unlink",
|
||||
"attributes": {
|
||||
"provider": "microsoft",
|
||||
"unlinked": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5) Endpoint Summary
|
||||
- `GET /api/v1/auth/microsoft/login`
|
||||
- `GET /api/v1/auth/microsoft/callback`
|
||||
- `POST /api/v1/auth/sso/link`
|
||||
- `DELETE /api/v1/auth/sso/unlink`
|
||||
|
||||
## 6) Error Handling (Frontend)
|
||||
Handle these API errors explicitly:
|
||||
- `401 Unauthorized`
|
||||
- `403 Forbidden`
|
||||
- `404 user not found`
|
||||
- `401 sso not linked`
|
||||
- `409 sso already linked`
|
||||
- `409 sso linked to another user`
|
||||
|
||||
Recommended behavior:
|
||||
- `401 sso not linked`: show message to login manually first, then link SSO in account settings.
|
||||
- `409` errors on link: inform user mapping conflict and stop retry loop.
|
||||
- `404 user not found`: force logout and ask user to re-authenticate.
|
||||
379
docs/aog-mcf-nsr-flow.md
Normal file
379
docs/aog-mcf-nsr-flow.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# Panduan Status Helikopter: AOG, MCF, NSR & Cara Menerbangkannya Lagi
|
||||
|
||||
> Dokumen ini menjelaskan **kapan sebuah helikopter berhenti boleh terbang**, **kenapa**, dan **langkah apa yang harus dilakukan supaya boleh terbang lagi** — lengkap dengan endpoint yang dipakai di tiap langkah.
|
||||
>
|
||||
> Ditulis supaya bisa dimengerti baik oleh tim operasional maupun developer. Bagian teknis (endpoint, field) ada di tiap langkah, tapi alurnya diceritakan dulu dengan bahasa sederhana.
|
||||
>
|
||||
> Semua endpoint berawalan `/api/v1` dan butuh login.
|
||||
|
||||
---
|
||||
|
||||
## 1. Istilah Penting (versi gampang)
|
||||
|
||||
Sebelum masuk alur, kenali dulu pemainnya:
|
||||
|
||||
| Istilah | Bahasa gampangnya |
|
||||
|---|---|
|
||||
| **Complaint** | Laporan kerusakan/keluhan pada helikopter. "Ada yang rusak nih." |
|
||||
| **MEL** (Minimum Equipment List) | Aturan: kerusakan ini boleh ditunda berapa lama sebelum pesawat **wajib** berhenti. Ada kelas A/B/C/D (makin besar = makin lama boleh ditunda). |
|
||||
| **NON-MEL** | Kerusakan yang **tidak boleh ditunda sama sekali** → pesawat langsung berhenti. |
|
||||
| **AOG** (Aircraft On Ground) | Status "pesawat dikandangkan, **tidak boleh terbang**". |
|
||||
| **NSR** (Non-Safety Release) | Keputusan: "kerusakan ini aman ditunda, **boleh terbang dulu**" walau belum diperbaiki. |
|
||||
| **Action Taken** | Tindakan perbaikan sementara yang dicatat, supaya pesawat boleh terbang lagi — tapi **belum dianggap selesai resmi**. |
|
||||
| **EASA Release** (CRS Part-145) | Surat resmi "**pesawat sudah diperbaiki & layak terbang**". Inilah yang benar-benar **menutup** kerusakan. |
|
||||
| **MCF** (Maintenance Check Flight) | Penerbangan uji setelah perbaikan besar. Selama wajib MCF, pesawat berstatus khusus. |
|
||||
| **Fleet Status** | Data kondisi tiap helikopter: jadwal perawatan, jam terbang, status, dll. |
|
||||
|
||||
**Inti yang paling sering bikin bingung** (akan diulang lagi nanti):
|
||||
> **Boleh terbang lagi** ≠ **kerusakan selesai.**
|
||||
> - Pesawat bisa **boleh terbang lagi** lewat *Action Taken* atau *NSR* — tapi laporannya **masih terbuka**.
|
||||
> - Kerusakan baru **benar-benar selesai (ditutup)** ketika **EASA Release ditandatangani (sign)** — bukan sekadar dibuat.
|
||||
|
||||
---
|
||||
|
||||
## 2. Status Helikopter & Urutannya
|
||||
|
||||
Helikopter selalu ada di salah satu dari 4 status ini. Status **dihitung otomatis** dari kondisi data (bukan diisi manual):
|
||||
|
||||
| Status | Artinya |
|
||||
|---|---|
|
||||
| `mcf` | Wajib penerbangan uji (Maintenance Check Flight) dulu |
|
||||
| `aog` | Dikandangkan, tidak boleh terbang |
|
||||
| `booked` | Sedang dipakai misi |
|
||||
| `available` | Sehat, siap terbang |
|
||||
|
||||
**Kalau beberapa kondisi terjadi bersamaan, yang menang urutannya begini:**
|
||||
|
||||
```
|
||||
MCF > AOG > Booked > Available
|
||||
```
|
||||
|
||||
Contoh: helikopter yang sedang wajib MCF **dan** punya kerusakan AOG → tetap tampil `mcf` (karena MCF paling tinggi). Ini sama persis dengan sistem Wucher lama.
|
||||
|
||||
---
|
||||
|
||||
## 3. Gambaran Besar: 3 Jalan Sebuah Helikopter Berhenti Terbang
|
||||
|
||||
Ada **3 sebab** helikopter jadi tidak boleh terbang. Tiap sebab punya cara keluar sendiri:
|
||||
|
||||
| Sebab berhenti | Jadi status | Cara terbang lagi |
|
||||
|---|---|---|
|
||||
| **A. Ada kerusakan** (complaint) | `aog` | EASA Release disign (atau ditunda dulu via Action/NSR) |
|
||||
| **B. Jadwal perawatan jatuh tempo** (otomatis) | `aog` | Perbarui jadwal/selesai inspeksi → hitung ulang |
|
||||
| **C. Wajib penerbangan uji** (MCF) | `mcf` | Selesaikan MCF dengan hasil "passed" |
|
||||
|
||||
Tiga sebab ini dibahas satu per satu di bawah.
|
||||
|
||||
---
|
||||
|
||||
## 4. JALAN A — Helikopter Rusak (Complaint → AOG → Perbaikan)
|
||||
|
||||
Ini alur paling sering. Ceritanya berurutan:
|
||||
|
||||
### Langkah 1 — Catat kerusakannya
|
||||
Begitu ada kerusakan, buat laporan (complaint).
|
||||
|
||||
`POST /complaints/create` (permission `complaint.create`)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"data": {
|
||||
"type": "complaint_create",
|
||||
"attributes": {
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"description": "Getaran di Engine 2 saat cruise",
|
||||
"mel_severity": 0, // lihat tabel di Langkah 2
|
||||
"aircraft_hours_at_report": "1245.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Langkah 2 — Tentukan seberapa parah (klasifikasi MEL)
|
||||
Field `mel_severity` menentukan **kapan pesawat harus berhenti**:
|
||||
|
||||
| `mel_severity` | Kelas | Boleh terbang sampai kapan? |
|
||||
|---|---|---|
|
||||
| `0` | NON-MEL | **Tidak boleh sama sekali** → AOG seketika |
|
||||
| `1` | A | Maksimal **1 hari**, lewat itu → AOG |
|
||||
| `2` | B | Maksimal **3 hari** → AOG |
|
||||
| `3` | C | Maksimal **10 hari** → AOG |
|
||||
| `4` | D | Maksimal **120 hari** → AOG |
|
||||
| *(dikosongkan)* | belum diklasifikasi | Statusnya `pending_classification`, **belum bikin AOG** sampai diklasifikasi |
|
||||
|
||||
> Klasifikasi bisa diisi belakangan lewat `PATCH /complaints/update/:id` (field `mel_severity`). Sistem mencatat siapa & kapan mengklasifikasi (audit `mel_classified_by/at`).
|
||||
|
||||
Selama kerusakan ini bikin AOG, di Fleet Status detail helikopter akan **muncul** data `complaints` + `easa`. Kalau sudah tidak AOG, dua data itu hilang sendiri.
|
||||
|
||||
### Langkah 3 (opsional) — Mau terbang dulu sebelum diperbaiki tuntas?
|
||||
Kadang pesawat perlu tetap operasi sambil menunggu perbaikan resmi. Ada **2 cara**, dan ini beda fungsi:
|
||||
|
||||
**Cara 3a — Action Taken (tindakan sementara sudah dilakukan):**
|
||||
`PATCH /complaints/update/:id` isi `action_taken`.
|
||||
|
||||
```jsonc
|
||||
{ "data": { "type": "complaint_update", "id": "<complaint_id>",
|
||||
"attributes": {
|
||||
"action_taken": "Kencangkan engine mount, cek torsi OK",
|
||||
"aircraft_hours_at_fix": "1250.1"
|
||||
} } }
|
||||
```
|
||||
→ Pesawat **boleh terbang lagi**, tapi laporan **masih terbuka**.
|
||||
|
||||
**Cara 3b — NSR (kerusakan aman ditunda):**
|
||||
`PATCH /complaints/update/:id` isi `is_nsr: true` (**butuh permission khusus `complaint.nsr`**).
|
||||
|
||||
```jsonc
|
||||
{ "data": { "type": "complaint_update", "id": "<complaint_id>",
|
||||
"attributes": {
|
||||
"is_nsr": true,
|
||||
"nsr_reason": "Dirilis di bawah MEL, defect ditunda"
|
||||
} } }
|
||||
```
|
||||
→ Pesawat **boleh terbang lagi**, laporan **masih terbuka**. Sistem mencatat siapa yang memutuskan NSR (`nsr_decided_by/at`).
|
||||
|
||||
> **Beda Action Taken vs NSR:**
|
||||
> - *Action Taken* = "sudah saya tangani sementara".
|
||||
> - *NSR* = "belum ditangani, tapi memang **boleh ditunda** dengan aman".
|
||||
> Keduanya sama-sama bikin pesawat boleh terbang lagi, dan **sama-sama belum menutup laporan**.
|
||||
>
|
||||
> Membatalkan NSR: kirim `is_nsr: false` → aturan AOG berlaku lagi.
|
||||
|
||||
### Langkah 4 — Tutup kerusakan secara resmi (EASA Release)
|
||||
Inilah langkah yang **benar-benar menyelesaikan** kerusakan. Tiga sub-langkah:
|
||||
|
||||
**4a. Buat surat EASA-nya** (boleh draft / belum lengkap dulu):
|
||||
`POST /easa-releases/create` (permission `easa_release.create`)
|
||||
|
||||
> ⚠️ **Syarat boleh membuat EASA:** semua complaint terbuka helikopter itu **harus sudah di-*Action Taken* atau di-NSR dulu** (Langkah 3). Kalau masih ada yang "mentah", create ditolak (`ErrEASAReleaseComplaintsPendingAction`). Jadi Langkah 3 itu **pintu masuk** ke Langkah 4.
|
||||
|
||||
**4b. Lengkapi datanya** (kalau tadi masih draft):
|
||||
`PATCH /easa-releases/update/:id`
|
||||
|
||||
**4c. TANDA TANGAN (sign) — ini momen kerusakan benar-benar ditutup:**
|
||||
`POST /easa-releases/sign/:id` (permission `easa_release.sign`, **tanpa body** — penandatangan = user yang sedang login).
|
||||
|
||||
```bash
|
||||
POST /api/v1/easa-releases/sign/019d6772-471c-7b70-b42a-d94020ef61e4
|
||||
```
|
||||
|
||||
**Sebelum bisa sign, 5 field ini WAJIB terisi:**
|
||||
|
||||
| Field | Contoh |
|
||||
|---|---|
|
||||
| `easa_date` | `2026-06-16` |
|
||||
| `easa_ac_hours` | `1245.3` |
|
||||
| `easa_location` | `LOWI` |
|
||||
| `easa_wo_no` | `WO-2026-001` |
|
||||
| `easa_signer_id` | `145.A.50` |
|
||||
|
||||
Kalau ada yang kosong → ditolak (`ErrEASAReleaseIncompleteForSign`) beserta daftar `missing_sign_fields`. Aturannya tegas: **EASA yang belum lengkap tidak boleh dianggap selesai.**
|
||||
|
||||
> **Jalan pintas:** kalau datanya sudah lengkap dari awal, kamu bisa langsung sign **saat create** — tambahkan `"sign": true` di body create. (Catatan: flag `sign` ini **hanya ada di create**, tidak ada di update.)
|
||||
|
||||
**Begitu sign berhasil, otomatis terjadi:**
|
||||
1. Surat EASA tercatat `signed_at` + `signed_by`.
|
||||
2. **Semua complaint terbuka** helikopter itu ditutup → statusnya jadi `serviced`.
|
||||
3. Karena tidak ada lagi kerusakan yang menggantung → **AOG hilang**, data `complaints`/`easa` di Fleet Status ikut hilang.
|
||||
4. Tercatat di riwayat (history): "Release to Service (EASA signed)".
|
||||
|
||||
### Ringkasan Jalan A (urut)
|
||||
```
|
||||
Rusak → buat complaint → klasifikasi MEL
|
||||
→ (opsional) Action Taken / NSR ➜ boleh terbang, laporan MASIH terbuka
|
||||
→ EASA create → EASA lengkapi → EASA SIGN ✅ ➜ laporan DITUTUP, AOG hilang
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. JALAN B — Jadwal Perawatan Jatuh Tempo (AOG Otomatis)
|
||||
|
||||
### Apa ini?
|
||||
Tiap helikopter punya jadwal inspeksi di Fleet Status (mis. inspeksi tiap 100 jam, atau jatuh tempo di tanggal tertentu). Kalau **jam terbang** atau **tanggal** sudah melewati batas, sistem **otomatis** men-AOG-kan pesawat — tanpa perlu ada yang lapor.
|
||||
|
||||
### Cara masuk AOG
|
||||
Diatur di Fleet Status, tiap jadwal punya:
|
||||
- `due` = batas normal (boleh berupa **jam**, mis. `1200`, atau **tanggal**, mis. `2026-09-01`)
|
||||
- `ext` = batas perpanjangan (kalau diisi, ini yang dipakai menggantikan `due`)
|
||||
|
||||
Saat jam/tanggal aktual melewati batas → status jadi `aog` dengan keterangan otomatis (`AOG: ... next due reached`).
|
||||
|
||||
> Engine yang tidak aktif di helikopter (flag `NR1`/`NR2` = false) **dilewati** — jadwal engine itu tidak ikut bikin AOG.
|
||||
|
||||
### Kapan sistem menghitung ulang?
|
||||
AOG jenis ini **disimpan**, jadi perlu pemicu untuk berubah:
|
||||
- Saat data penerbangan (flight data) dibuat/diubah/dihapus.
|
||||
- Saat Fleet Status helikopter itu di-update.
|
||||
- **Otomatis tiap 1 jam** (cron) — supaya AOG berbasis tanggal tetap nyala walau tidak ada aktivitas.
|
||||
|
||||
### Cara terbang lagi
|
||||
Perbarui jadwalnya supaya tidak lagi lewat tempo (mis. setelah inspeksi selesai, isi `due`/`ext` yang baru):
|
||||
|
||||
`PATCH /fleet-status/update/:id`
|
||||
```jsonc
|
||||
{ "data": { "type": "fleet_status_update", "id": "<fleet_status_id>",
|
||||
"attributes": { "maintenance_schedules": [
|
||||
{ "inspection_type": "100H", "due": "1400", "ext": "" }
|
||||
] } } }
|
||||
```
|
||||
Pada perhitungan ulang berikutnya, AOG otomatis ini dilepas. (Catatan: sistem hanya melepas AOG yang dibuatnya sendiri secara otomatis; AOG manual tidak disentuh.)
|
||||
|
||||
---
|
||||
|
||||
## 6. JALAN C — Wajib Penerbangan Uji (MCF)
|
||||
|
||||
### Apa ini?
|
||||
Setelah pekerjaan perawatan besar, helikopter kadang wajib menjalani **penerbangan uji (MCF)** dulu sebelum dianggap normal. Selama wajib MCF, statusnya `mcf` (mengalahkan AOG).
|
||||
|
||||
### Langkah 1 — Tandai wajib MCF (cukup minimal)
|
||||
`POST /mcf/create` (permission `mcf.create`) — body cukup `helicopter_id` saja (+ `notes` opsional). Tujuannya hanya menandai pesawat ini wajib MCF.
|
||||
|
||||
```jsonc
|
||||
{ "data": { "type": "mcf_create",
|
||||
"attributes": {
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"notes": "Wajib MCF setelah kerja engine mount"
|
||||
} } }
|
||||
```
|
||||
|
||||
### Langkah 2 — Selesaikan MCF setelah terbang uji
|
||||
`POST /mcf/complete/:helicopter_id` (permission `mcf.complete`) — **pakai helicopter_id**, bukan id MCF. Wajib isi semua field hasil uji:
|
||||
|
||||
```jsonc
|
||||
{ "data": { "type": "mcf_complete",
|
||||
"attributes": {
|
||||
"af_hours": "1245.3",
|
||||
"landing_count": 3,
|
||||
"utc_time": "2026-06-17T08:30:00Z", // format RFC3339
|
||||
"date": "2026-06-17", // format YYYY-MM-DD
|
||||
"result": "passed", // passed atau failed
|
||||
"notes": "Getaran dalam batas normal"
|
||||
} } }
|
||||
```
|
||||
|
||||
**Hasilnya menentukan:**
|
||||
|
||||
| `result` | Yang terjadi |
|
||||
|---|---|
|
||||
| `passed` | MCF beres → pesawat **lepas dari status MCF** (kembali ke status lain sesuai kondisi) |
|
||||
| `failed` | Pesawat **tetap MCF**, hasil gagal dicatat ke riwayat → ulangi sampai `passed` |
|
||||
|
||||
> Catatan: kalau tidak ada MCF terbuka untuk helikopter itu, complete ditolak (`409`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Melihat Riwayat (History)
|
||||
|
||||
Semua kejadian penting di atas — dari kerusakan dilaporkan, diklasifikasi, NSR, perbaikan, EASA disign, hasil MCF, sampai AOG otomatis nyala/mati — **tercatat otomatis** dalam satu garis waktu (timeline) per helikopter. Berguna untuk audit: *siapa* melakukan *apa* dan *kapan*.
|
||||
|
||||
### Di mana lihatnya?
|
||||
`GET /fleet-status/history/:helicopter_id` (permission `flight.read`)
|
||||
|
||||
```bash
|
||||
GET /api/v1/fleet-status/history/019d6771-5fb5-7337-837f-bc5ed85181a1?page=1&page_size=20
|
||||
```
|
||||
|
||||
- Diurutkan **terbaru di atas**.
|
||||
- Mendukung halaman: `page` (default 1) & `page_size` (default 20).
|
||||
- Timeline ini **gabungan dua sumber**: catatan perawatan yang diselesaikan (service log) + semua kejadian aksi (complaint, EASA, MCF, AOG).
|
||||
|
||||
### Kejadian apa saja yang tercatat?
|
||||
|
||||
| `action` | Tercatat ketika… |
|
||||
|---|---|
|
||||
| `complaint_filed` | Laporan kerusakan dibuat |
|
||||
| `mel_classified` | Kerusakan diklasifikasi MEL (A/B/C/D atau NON-MEL) |
|
||||
| `nsr_done` | NSR diberikan (boleh terbang walau defect terbuka) |
|
||||
| `nsr_cancelled` | NSR dibatalkan |
|
||||
| `action_taken` | Tindakan perbaikan sementara dicatat |
|
||||
| `easa_created` | Surat EASA dibuat (draft) |
|
||||
| `easa_signed` | **EASA ditandatangani → kerusakan ditutup** |
|
||||
| `mcf_result` | Hasil MCF diisi (`passed`/`failed`) |
|
||||
| `aog_set` | AOG otomatis dinyalakan (jadwal jatuh tempo) |
|
||||
| `aog_cleared` | AOG otomatis dilepas |
|
||||
| `serviced` | Inspeksi/perawatan diselesaikan |
|
||||
|
||||
### Bentuk responsnya
|
||||
|
||||
Tiap baris timeline berisi: kapan, jenisnya, aksinya, pesan, dan **siapa pelakunya** (nama langsung tersedia di `actor_name`).
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"at": "2026-06-17T10:15:00Z",
|
||||
"entity_type": "easa_release",
|
||||
"entity_id": "019d6772-471c-7b70-b42a-d94020ef61e4",
|
||||
"action": "easa_signed",
|
||||
"message": "Release to Service (EASA signed)",
|
||||
"actor": "019e91e6-2908-7f37-93e6-015ed1b7587d",
|
||||
"actor_name": "John Doe"
|
||||
},
|
||||
{
|
||||
"at": "2026-06-17T09:40:00Z",
|
||||
"entity_type": "complaint",
|
||||
"action": "action_taken",
|
||||
"message": "Action taken — Kencangkan engine mount, cek torsi OK",
|
||||
"actor_name": "Jane Tech"
|
||||
}
|
||||
],
|
||||
"meta": { "page_number": 1, "page_size": 20, "total": 12 }
|
||||
}
|
||||
```
|
||||
|
||||
> Selain timeline ini, daftar **kerusakan per helikopter** (beserta status terkini, NSR, action, dan EASA terkait) bisa dilihat lewat `GET /complaints/get-by-helicopter/:helicopter_id`, dan kondisi terkini helikopter (status + complaint + EASA aktif) lewat `GET /fleet-status/get-by-helicopter/:helicopter_id`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tabel Endpoint Ringkas
|
||||
|
||||
| Mau apa | Endpoint | Permission |
|
||||
|---|---|---|
|
||||
| Lapor kerusakan | `POST /complaints/create` | `complaint.create` |
|
||||
| Klasifikasi MEL / Action Taken / NSR | `PATCH /complaints/update/:id` | `complaint.update` (+`complaint.nsr` utk NSR) |
|
||||
| Lihat kerusakan per helikopter | `GET /complaints/get-by-helicopter/:helicopter_id` | `complaint.read` |
|
||||
| Buat surat EASA | `POST /easa-releases/create` | `easa_release.create` |
|
||||
| Lengkapi EASA | `PATCH /easa-releases/update/:id` | `easa_release.update` |
|
||||
| **Tanda tangan EASA (tutup kerusakan)** | `POST /easa-releases/sign/:id` | `easa_release.sign` |
|
||||
| Tandai wajib MCF | `POST /mcf/create` | `mcf.create` |
|
||||
| **Selesaikan MCF** | `POST /mcf/complete/:helicopter_id` | `mcf.complete` |
|
||||
| Atur jadwal perawatan & jam | `POST /fleet-status/create` · `PATCH /fleet-status/update/:id` | `flight.create` / `flight.update` |
|
||||
| Lihat status + complaint + EASA | `GET /fleet-status/get-by-helicopter/:helicopter_id` | `flight.read` |
|
||||
| Lihat riwayat helikopter | `GET /fleet-status/history/:helicopter_id` | `flight.read` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Tanya–Jawab Cepat (FAQ)
|
||||
|
||||
**T: Kerusakan dianggap selesai saat EASA dibuat atau saat disign?**
|
||||
J: Saat **disign**. Membuat (create) atau melengkapi (update) EASA **belum** menutup apa pun. Penutupan complaint terjadi tepat di endpoint `sign`.
|
||||
|
||||
**T: Bedanya Action Taken, NSR, dan EASA Sign?**
|
||||
J:
|
||||
- **Action Taken** & **NSR** → bikin pesawat **boleh terbang lagi**, tapi laporan **masih terbuka**.
|
||||
- **EASA Sign** → **menutup** laporan secara resmi (status `serviced`).
|
||||
|
||||
**T: Kalau cuma isi Action Taken, kapan laporannya hilang dari daftar AOG?**
|
||||
J: AOG-nya langsung hilang begitu Action Taken diisi (pesawat boleh terbang). Tapi laporannya **tetap tercatat terbuka** sampai EASA disign.
|
||||
|
||||
**T: Kenapa saya tidak bisa membuat EASA?**
|
||||
J: Kemungkinan masih ada complaint yang belum di-Action/NSR. Selesaikan Langkah 3 dulu untuk tiap complaint terbuka.
|
||||
|
||||
**T: Helikopter wajib MCF tapi juga ada kerusakan AOG, tampil yang mana?**
|
||||
J: Tampil `mcf`, karena MCF prioritas tertinggi.
|
||||
|
||||
**T: AOG karena jadwal jatuh tempo kok tidak langsung hilang setelah saya update jadwal?**
|
||||
J: AOG jenis ini dihitung ulang saat ada flight data baru, update fleet status, atau otomatis tiap 1 jam. Tunggu pemicu berikutnya.
|
||||
|
||||
**T: Di mana saya bisa lihat riwayat apa saja yang sudah terjadi pada satu helikopter?**
|
||||
J: Di `GET /fleet-status/history/:helicopter_id` — timeline lengkap (terbaru di atas) berisi siapa melakukan apa & kapan. Lihat §7.
|
||||
|
||||
---
|
||||
|
||||
## 10. Catatan untuk Developer
|
||||
|
||||
- **AOG karena kerusakan** dihitung *saat dibaca* (real-time dari complaint terbuka) → berubah instan, tidak perlu job.
|
||||
- **AOG karena jadwal** *disimpan* di helikopter → perlu pemicu (flight data / update fleet / cron 1 jam). Hanya AOG otomatis yang dilepas otomatis; AOG manual tidak.
|
||||
- **MCF mengalahkan AOG** dalam prioritas status.
|
||||
- **Penutupan kerusakan hanya lewat EASA sign** — `action_taken` & `is_nsr` hanya mengangkat larangan terbang, tidak menutup laporan.
|
||||
- **Masa tenggang MEL dihitung dalam HARI** (A=1, B=3, C=10, D=120) di `internal/domain/complaint/model.go` fungsi `MELDeadline` (`* 24 * time.Hour`).
|
||||
49
docs/audit-log.md
Normal file
49
docs/audit-log.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Audit Log
|
||||
|
||||
## Tujuan
|
||||
|
||||
- Menyimpan jejak request API dan event penting service ke tabel `audit_logs`.
|
||||
- Bersifat append-only.
|
||||
- Tidak menyimpan secret sensitif (password, raw token).
|
||||
|
||||
## Desain
|
||||
|
||||
- Semua route di bawah `/api/*` dicatat otomatis oleh middleware `AuditMiddleware`.
|
||||
- Penyimpanan ke DB dilakukan async melalui `AuditLogService` (buffered queue + worker) agar request API tetap cepat.
|
||||
- Jika queue penuh, request tetap lanjut (fail-open) dan sistem menulis warning log.
|
||||
|
||||
## Schema Ringkas (`audit_logs`)
|
||||
|
||||
- `id` (BINARY(16), PK)
|
||||
- `request_id` (indexed)
|
||||
- `actor_user_id` (nullable, indexed)
|
||||
- `layer` (`api` / `service`)
|
||||
- `action` (indexed)
|
||||
- `method`, `path`, `status_code`, `success`
|
||||
- `ip`, `user_agent`
|
||||
- `message`
|
||||
- `metadata` (JSON)
|
||||
- `created_at`
|
||||
|
||||
## Helper Pemakaian di Service
|
||||
|
||||
Gunakan helper yang sudah ada di `AuthService`:
|
||||
|
||||
```go
|
||||
s.logServiceAudit(ctx, "auth.reset_password.consume", userID, true, "password_reset_success", map[string]any{
|
||||
"reason": "manual_reset",
|
||||
})
|
||||
```
|
||||
|
||||
Untuk service lain, inject `*service.AuditLogService` lalu panggil:
|
||||
|
||||
```go
|
||||
auditSvc.Log(ctx, service.AuditLogInput{
|
||||
Layer: "service",
|
||||
Action: "your.service.action",
|
||||
ActorUserID: userID,
|
||||
Success: true,
|
||||
Message: "done",
|
||||
Metadata: map[string]any{"key": "value"},
|
||||
})
|
||||
```
|
||||
272
docs/auth-flow.md
Normal file
272
docs/auth-flow.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Auth Flow
|
||||
|
||||
This document describes the recommended authentication flows in Wucher. All API responses follow **JSON:API**.
|
||||
|
||||
## 1) Email/Password Registration
|
||||
|
||||
### 1.1 Register
|
||||
`POST /api/v1/auth/register`
|
||||
|
||||
Notes:
|
||||
- The user role is assigned based on `AUTH_DEFAULT_ROLE`.
|
||||
- For production, you can disable this endpoint by setting `AUTH_DISABLE_REGISTER=true`.
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_register",
|
||||
"attributes": {
|
||||
"email": "user@example.com",
|
||||
"password": "StrongP@ssw0rd",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"mobile_phone": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response: `201 Created` with user resource.
|
||||
|
||||
### 1.2 Verify Email
|
||||
User receives a verification link in email:
|
||||
`GET /api/v1/auth/verify-email?token=...`
|
||||
|
||||
Response: `200 OK` with `{ verified: true }`.
|
||||
|
||||
### 1.3 Login
|
||||
`POST /api/v1/auth/login`
|
||||
|
||||
If TOTP **is not enabled**, response is `200 OK` and cookies are set:
|
||||
- `wucher_at` (access)
|
||||
- `wucher_rt` (refresh)
|
||||
|
||||
If TOTP **is enabled**, response is `202 Accepted` with `challenge_token`.
|
||||
|
||||
### 1.4 Forgot Password
|
||||
`POST /api/v1/auth/forgot-password`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_forgot_password",
|
||||
"attributes": {
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response is always `200 OK` with a generic message:
|
||||
`If the account exists, a password reset link has been sent.`
|
||||
|
||||
### 1.5 Reset Password
|
||||
`POST /api/v1/auth/reset-password`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_password",
|
||||
"attributes": {
|
||||
"token": "<token>",
|
||||
"new_password": "StrongPassphrase123!",
|
||||
"confirm_password": "StrongPassphrase123!"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success response: `200 OK` with message instructing the user to log in again.
|
||||
|
||||
Invalid/expired/used token response: `400 Bad Request` with:
|
||||
`This reset link is invalid or expired.`
|
||||
|
||||
### 1.6 Security PIN for Sensitive Actions
|
||||
|
||||
Security PIN endpoints:
|
||||
- `POST /api/v1/auth/pin/setup`
|
||||
- `POST /api/v1/auth/pin/verify`
|
||||
- `POST /api/v1/auth/pin/change`
|
||||
- `POST /api/v1/auth/pin/forgot`
|
||||
- `POST /api/v1/auth/pin/reset`
|
||||
|
||||
For `POST /api/v1/auth/pin/reset`:
|
||||
- If `method=password`, send the reset `token` from the email link plus `password`.
|
||||
- If `method=microsoft`, send only `reauth_token` from Microsoft re-auth.
|
||||
|
||||
Sensitive actions require header:
|
||||
- `X-PIN-Verification-Token: <one-time-token>`
|
||||
|
||||
`auth/pin/verify` menggunakan `action` berupa UUID opaque (transaction ID), bukan permission key seperti `user.delete`.
|
||||
|
||||
Detailed flow + full payload examples:
|
||||
- See [docs/security-pin.md](security-pin.md)
|
||||
|
||||
## 2) TOTP Setup (Enable 2FA)
|
||||
|
||||
### 2.1 Setup
|
||||
`POST /api/v1/auth/totp/setup`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_totp_setup",
|
||||
"attributes": {
|
||||
"user_id": "<uuid>",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response: `200 OK` with `secret` and `otpauth_url`.
|
||||
> TOTP is **not** enabled yet (pending).
|
||||
|
||||
### 2.2 Confirm
|
||||
`POST /api/v1/auth/totp/confirm`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_totp_confirm",
|
||||
"attributes": {
|
||||
"user_id": "<uuid>",
|
||||
"code": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response: `200 OK` and TOTP becomes **enabled**.
|
||||
|
||||
## 3) TOTP Login (2FA)
|
||||
|
||||
### 3.1 Login
|
||||
`POST /api/v1/auth/login`
|
||||
|
||||
Response: `202 Accepted` with `challenge_token`.
|
||||
|
||||
### 3.2 Verify
|
||||
`POST /api/v1/auth/totp/verify`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_totp_verify",
|
||||
"attributes": {
|
||||
"challenge_token": "<token>",
|
||||
"code": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response: `200 OK` and cookies are set.
|
||||
|
||||
## 4) Microsoft Entra SSO
|
||||
|
||||
### 4.1 Start Login
|
||||
`GET /api/v1/auth/microsoft/login`
|
||||
|
||||
Response: `200 OK` with JSON:API body (`type: auth_microsoft_url`) containing `url` and `redirect_url`
|
||||
|
||||
### 4.2 Callback
|
||||
Microsoft redirects to:
|
||||
`GET /api/v1/auth/microsoft/callback?code=...&state=...`
|
||||
|
||||
Backend behavior:
|
||||
- If SSO mapping (`provider + provider_subject`) is linked to an existing user, login succeeds.
|
||||
- If mapping is missing, login is rejected (`401 SSO not linked`).
|
||||
- If mapping exists but user record is missing, login is rejected (`404 user not found`).
|
||||
- No auto-register on SSO callback.
|
||||
|
||||
On success, backend sets cookies and may redirect to:
|
||||
`AUTH_SSO_SUCCESS_REDIRECT`
|
||||
|
||||
### 4.3 Link SSO (authenticated user)
|
||||
`POST /api/v1/auth/sso/link`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_link",
|
||||
"attributes": {
|
||||
"provider": "microsoft",
|
||||
"code": "<microsoft_authorization_code>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- 1 user only 1 SSO mapping.
|
||||
- Same SSO account cannot be linked to another user.
|
||||
|
||||
### 4.4 Unlink SSO (authenticated user)
|
||||
`DELETE /api/v1/auth/sso/unlink`
|
||||
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_sso_unlink",
|
||||
"attributes": {
|
||||
"provider": "microsoft"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5) WebAuthn (Passkey)
|
||||
|
||||
### 5.1 Register Credential (Authenticated User)
|
||||
1. `POST /api/v1/auth/webauthn/register/begin` (auth required)
|
||||
2. Browser executes `navigator.credentials.create(...)` using `public_key` from response
|
||||
3. `POST /api/v1/auth/webauthn/register/finish` with `challenge_token` + `credential`
|
||||
|
||||
### 5.2 Login with WebAuthn
|
||||
1. `POST /api/v1/auth/webauthn/login/begin` with email
|
||||
2. Browser executes `navigator.credentials.get(...)` using `public_key` from response
|
||||
3. `POST /api/v1/auth/webauthn/login/finish` with `challenge_token` + `credential`
|
||||
4. On success, backend sets auth cookies.
|
||||
|
||||
## 6) Refresh & Logout
|
||||
|
||||
### Refresh
|
||||
`POST /api/v1/auth/refresh`
|
||||
|
||||
Uses refresh cookie to issue new tokens.
|
||||
|
||||
### Logout
|
||||
`POST /api/v1/auth/logout`
|
||||
|
||||
Revokes refresh token and clears cookies.
|
||||
|
||||
Frontend note:
|
||||
- Use `credentials: 'include'` on requests that depend on auth cookies.
|
||||
- Keep app logout local-only; do not redirect to Microsoft logout from this endpoint.
|
||||
- Microsoft front-channel logout is handled separately at `/api/v1/auth/microsoft/front-channel-logout` and is designed to work in an iframe.
|
||||
|
||||
## Notes
|
||||
- Access token is stored in HttpOnly cookie: `AUTH_JWT_ACCESS_COOKIE`
|
||||
- Refresh token is stored in HttpOnly cookie: `AUTH_JWT_REFRESH_COOKIE`
|
||||
- Cookies are configured by `AUTH_JWT_COOKIE_*` envs
|
||||
- Microsoft SSO sessions use `SameSite=None; Secure` so front-channel iframe logout can receive cookies.
|
||||
- Access token TTL is controlled by `AUTH_JWT_ACCESS_TTL` (default `10m`)
|
||||
- Refresh token TTL is controlled by `AUTH_JWT_REFRESH_TTL` (default `1h`)
|
||||
- TOTP challenge tokens are stored server-side in MySQL-backed runtime storage for `AUTH_TOTP_CHALLENGE_TTL`
|
||||
- Password-reset tokens are stored in MySQL as SHA-256 hashes (raw token is never stored)
|
||||
- Password-reset tokens are single-use and expire by `AUTH_PASSWORD_RESET_TTL` (default 20m)
|
||||
- Forgot-password endpoint has per-IP and per-email throttling
|
||||
- Login (including SSO callback and post-TOTP token issue) bumps per-user session version, so a new login invalidates previous active sessions
|
||||
- Successful password reset revokes active sessions by bumping per-user session version
|
||||
- PIN can be blocked after max failed attempts; use `POST /api/v1/auth/pin/request-reset` (auth required) to request reset email for blocked accounts
|
||||
- WebAuthn challenge token is single-use, stored server-side, and expires by `AUTH_WEBAUTHN_CHALLENGE_TTL`.
|
||||
- WebAuthn credential verification enforces RP ID + RP origin checks from `AUTH_WEBAUTHN_RP_ID` and `AUTH_WEBAUTHN_RP_ORIGINS`.
|
||||
146
docs/base-contact-roles-frontend-flow.md
Normal file
146
docs/base-contact-roles-frontend-flow.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Base Contact Roles Frontend Flow (English)
|
||||
|
||||
This document explains the frontend flow for contact assignment in the **Bases** module:
|
||||
- `responsible_flight_rescuer_contact_ids` (multi-contact)
|
||||
- `chief_physician_in_charge_contact_ids` (multi-contact)
|
||||
|
||||
Contact source is always based on **contact id (`users.id`)**.
|
||||
|
||||
## 1) Data Source for Contact Dropdowns
|
||||
|
||||
Get contact candidates from contacts API:
|
||||
- `GET /api/v1/contacts/get-all?role=air_rescuer&limit=200`
|
||||
|
||||
Minimum fields for FE:
|
||||
- `id` (resource id) => send this in base payload
|
||||
- `attributes.first_name`, `attributes.last_name`, `attributes.short_name` (display label)
|
||||
- optional: `attributes.is_active`
|
||||
|
||||
UI suggestion:
|
||||
- Use 2 separate multi-select components:
|
||||
- Responsible Flight Rescuer
|
||||
- Chief Physician In Charge
|
||||
- The same person may exist in both lists if business rules allow it.
|
||||
|
||||
## 2) Create Base Flow
|
||||
|
||||
Endpoint:
|
||||
- `POST /api/v1/bases/create`
|
||||
|
||||
Example payload:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "base",
|
||||
"attributes": {
|
||||
"base": "Lude",
|
||||
"base_category": "regular",
|
||||
"responsible_flight_rescuer_contact_ids": [
|
||||
"0196a111-1111-7abc-9def-111111111111",
|
||||
"0196a222-2222-7abc-9def-222222222222"
|
||||
],
|
||||
"chief_physician_in_charge_contact_ids": [
|
||||
"0196a333-3333-7abc-9def-333333333333"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Backend behavior:
|
||||
- each id must be a valid UUID and exist in `users`
|
||||
- invalid UUID / missing user id => request fails (`4xx`)
|
||||
|
||||
## 3) Update Base Flow
|
||||
|
||||
Endpoint:
|
||||
- `PATCH /api/v1/bases/update/{uuid}`
|
||||
|
||||
Payload to update assignment only:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "base_update",
|
||||
"attributes": {
|
||||
"responsible_flight_rescuer_contact_ids": [
|
||||
"0196a111-1111-7abc-9def-111111111111"
|
||||
],
|
||||
"chief_physician_in_charge_contact_ids": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Important notes:
|
||||
- update uses **replace** semantics per base (not append)
|
||||
- always submit the final arrays from current UI state
|
||||
- send `[]` to clear all assignments for that role
|
||||
|
||||
## 4) Load Base Detail into Edit Form
|
||||
|
||||
Endpoint:
|
||||
- `GET /api/v1/bases/get/{uuid}`
|
||||
|
||||
Response fields used by FE:
|
||||
- `data.attributes.responsible_flight_rescuer_contact_ids`
|
||||
- `data.attributes.chief_physician_in_charge_contact_ids`
|
||||
- `data.attributes.responsible_flight_rescuers` (id + first_name + last_name)
|
||||
- `data.attributes.chief_physicians_in_charge` (id + first_name + last_name)
|
||||
|
||||
FE flow:
|
||||
1. Load base detail.
|
||||
2. Load contact options for dropdown.
|
||||
3. Map selected ids to option objects for selected state.
|
||||
4. Use detailed arrays to show human-readable names immediately.
|
||||
|
||||
## 5) Show Base Roles in Contacts Module
|
||||
|
||||
Contacts API now includes:
|
||||
- `attributes.base_roles` (array)
|
||||
|
||||
Item format:
|
||||
```json
|
||||
{
|
||||
"base_id": "0196b111-1111-7abc-9def-111111111111",
|
||||
"base_name": "Lude",
|
||||
"role_code": "responsible_flight_rescuer"
|
||||
}
|
||||
```
|
||||
|
||||
Affected endpoints:
|
||||
- `GET /api/v1/contacts/get-all`
|
||||
- `GET /api/v1/contacts/get-all/dt`
|
||||
- `GET /api/v1/contacts/{user_id}`
|
||||
|
||||
Suggested label mapping in UI:
|
||||
- `responsible_flight_rescuer` => `Responsible Flight Rescuer`
|
||||
- `chief_physician_in_charge` => `Chief Physician In Charge`
|
||||
|
||||
## 6) UX Recommendations
|
||||
|
||||
- In contact detail page, group `base_roles` by `base_name`.
|
||||
- In contact list, show compact chips, e.g.:
|
||||
- `Lude (Responsible Flight Rescuer)`
|
||||
- `Berlin (Chief Physician In Charge)`
|
||||
- For large lists, show only first N chips + `+X more`.
|
||||
|
||||
## 7) Minimum Error Handling
|
||||
|
||||
- If base submit fails because UUID validation fails:
|
||||
- show general message: `Invalid contact assignment`.
|
||||
- If contact id no longer exists:
|
||||
- show message: `Some contacts are no longer available, please refresh`.
|
||||
- After successful create/update:
|
||||
- refresh base detail to keep assignment state synced with backend.
|
||||
|
||||
## 8) Compatibility Note
|
||||
|
||||
For backward compatibility, both ID arrays and detailed name arrays are returned:
|
||||
- `responsible_flight_rescuer_contact_ids`
|
||||
- `responsible_flight_rescuers`
|
||||
- `chief_physician_in_charge_contact_ids`
|
||||
- `chief_physicians_in_charge`
|
||||
|
||||
Recommended FE usage:
|
||||
- Use `..._contact_ids` for submit payload.
|
||||
- Use detailed arrays for display names.
|
||||
980
docs/collections/Wucher.json
Normal file
980
docs/collections/Wucher.json
Normal file
@@ -0,0 +1,980 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Wucher",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Role",
|
||||
"item": [
|
||||
{
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/create",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"create"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"description\": \"Testing.\",\n \"name\": \"role_test\"\n },\n \"type\": \"role\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/delete/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"delete",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "019c31eb-5750-7c71-b6b9-042282c61bab"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get All DT",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/get-all/dt?draw=1&length=5&start",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"get-all",
|
||||
"dt?draw=1&length=5&start"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "draw",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "length",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"key": "start",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "search",
|
||||
"value": "sta"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get All",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/get-all",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"get-all"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get By ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/get/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"get",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "019c31eb-5750-7c71-b6b9-042282c61bab"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/update/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"update",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "019c31eb-5750-7c71-b6b9-042282c61bab"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"description\": \"Testing update.\",\n \"name\": \"test_role\"\n },\n \"id\": \"019c31eb-5750-7c71-b6b9-042282c61bab\",\n \"type\": \"role\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Permission",
|
||||
"item": [
|
||||
{
|
||||
"name": "Assign",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/:uuid/permissions/assign-bulk",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
":uuid",
|
||||
"permissions",
|
||||
"assign-bulk"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"role_assign_permission_bulk\",\n \"attributes\": {\n \"permission_ids\": [\n \"019c79bd-c7a5-703d-8b7d-cd935e7e5316\",\n \"019c79bd-c7a5-720e-ad50-989d8848e37c\"\n ]\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get All Permission",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/permissions/get-all",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
"permissions",
|
||||
"get-all"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Permission of Role",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/:uuid/permissions",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
":uuid",
|
||||
"permissions"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": "019c30e6-1b8a-7714-898a-6e954e3dad33"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Remove Permission",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/roles/:uuid/permissions/remove-bulk",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"roles",
|
||||
":uuid",
|
||||
"permissions",
|
||||
"remove-bulk"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": "019c30e6-1b8a-7714-898a-6e954e3dad33"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"role_remove_permission_bulk\",\n \"attributes\": {\n \"permission_ids\": [\n \"019c79bd-c7a5-7974-ba89-69d52f0f3712\",\n \"019c79bd-c7a5-7339-add2-5a1c900cf1c3\"\n ]\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "User",
|
||||
"item": [
|
||||
{
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/create",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"create"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"user\",\n \"attributes\": {\n \"email\": \"marcomelandri808@gmail.com\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"mobile_phone\": \"+628123456789\",\n \"role_id\": \"019c30e6-1b8a-7714-898a-6e954e3dad33\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/delete/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"delete",
|
||||
":uuid"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": "019c79da-8c32-785c-b547-beee3b321e9b"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get All DT",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/get-all/dt",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"get-all",
|
||||
"dt"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get All",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/get-all",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"get-all"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get By UUID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/get/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"get",
|
||||
":uuid"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": "019c7940-c4c2-7fa4-ae0b-4eeb9e75295b"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/users/update/:uuid",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"users",
|
||||
"update",
|
||||
":uuid"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "uuid",
|
||||
"value": "019c7940-c4c2-7fa4-ae0b-4eeb9e75295b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"user\",\n \"id\": \"019c7940-c4c2-7fa4-ae0b-4eeb9e75295b\",\n \"attributes\": {\n \"first_name\": \"Johnny\",\n \"last_name\": \"Doe\",\n \"mobile_phone\": \"+628123456700\",\n \"role_id\": \"019c30e6-1b8a-7714-898a-6e954e3dad33\"\n }\n }\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"item": [
|
||||
{
|
||||
"name": "Auth Me",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/me",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"me"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Invite",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/invite",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"invite"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"auth_invite\",\n \"attributes\": {\n \"email\": \"marcomelandri808@gmail.com\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"mobile_phone\": \"+628123456789\",\n \"role_id\": \"019c30e6-1b8a-7714-898a-6e954e3dad33\"\n }\n }\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Login",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/login",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"login"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"auth_login\",\n \"attributes\": {\n \"email\": \"marcomelandri808@gmail.com\",\n \"password\": \"StrongP@ssw0rd!\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Logout",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/logout",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"logout"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Refresh Token",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/refresh",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"refresh"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Register",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/register",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"register"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"auth_register\",\n \"attributes\": {\n \"email\": \"innovatexsh@gmail.com\",\n \"password\": \"securePass123\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"mobile_phone\": \"087782553442\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Set New Password",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/set-password",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"set-password"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"type\": \"auth_set_password\",\n \"attributes\": {\n \"token\": \"--TpCbfl1JEpEJq1jr2l1XeFggiAN7Z2k23pwMVrxac\",\n \"password\": \"StrongP@ssw0rd!\"\n }\n }\n}\n",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SSO",
|
||||
"item": [
|
||||
{
|
||||
"name": "MS Login",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/microsoft/login",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"microsoft",
|
||||
"login"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TOTP",
|
||||
"item": [
|
||||
{
|
||||
"name": "TOTP Disable",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/totp/disable",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"totp",
|
||||
"disable"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"user_id\": \"019c2e21-d880-7b9a-89ac-3d3d8593502f\"\n },\n \"type\": \"auth_totp_disable\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TOTP Setup",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/totp/setup",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"totp",
|
||||
"setup"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"email\": \"innovatexsh@gmail.com\",\n \"user_id\": \"019c2e21-d880-7b9a-89ac-3d3d8593502f\"\n },\n \"type\": \"auth_totp_setup\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TOTP Verify Login",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/totp/verify",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"totp",
|
||||
"verify"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"challenge_token\": \"Z_2A0lm5IekOiIBPbpiJzpuevFYW6oxEIgdMZjPkcpw\",\n \"code\": \"568754\"\n },\n \"type\": \"auth_totp_verify\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TOTP Verify",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"description": "",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/v1/auth/totp/confirm",
|
||||
"protocol": "",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"v1",
|
||||
"auth",
|
||||
"totp",
|
||||
"confirm"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"attributes\": {\n \"code\": \"898427\",\n \"user_id\": \"019c2e21-d880-7b9a-89ac-3d3d8593502f\"\n },\n \"type\": \"auth_totp_confirm\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "",
|
||||
"type": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
359
docs/contact-create-flow.md
Normal file
359
docs/contact-create-flow.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# Contact Create Flow (Frontend Guide)
|
||||
|
||||
This document explains the full flow to create a contact, send an automatic invite email, and let the invited user set their first password.
|
||||
|
||||
## 1) Prerequisites
|
||||
|
||||
1. Admin user is already logged in and has an `access token`.
|
||||
2. Admin user has already configured a Security PIN.
|
||||
3. Frontend knows the target `role_id` (fetch from `GET /api/v1/roles/get-all`).
|
||||
|
||||
## 2) Endpoints Used
|
||||
|
||||
1. `POST /api/v1/auth/pin/verify` to get a PIN `action_token`.
|
||||
2. `POST /api/v1/contacts/create` to create the contact (auto invite enabled).
|
||||
3. `POST /api/v1/auth/set-password` to set the first password from invite token.
|
||||
|
||||
## 3) Detailed Flow
|
||||
|
||||
### Step A - Verify PIN for contact create action
|
||||
|
||||
Action ID for contact create:
|
||||
- `2a59e8b4-5a72-4be3-b8fc-4f49941181c3`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_verify",
|
||||
"attributes": {
|
||||
"pin": "123456",
|
||||
"action": "2a59e8b4-5a72-4be3-b8fc-4f49941181c3"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_verify",
|
||||
"attributes": {
|
||||
"verified": true,
|
||||
"action_token": "<PIN_ACTION_TOKEN>",
|
||||
"action_token_ttl_ms": 300000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Store `action_token`, then pass it to contact create in header:
|
||||
- `X-PIN-Verification-Token: <PIN_ACTION_TOKEN>`
|
||||
|
||||
### Step B - Create Contact (auto invite)
|
||||
|
||||
Endpoint:
|
||||
- `POST /api/v1/contacts/create`
|
||||
|
||||
Required headers:
|
||||
- `Authorization: Bearer <ACCESS_TOKEN>`
|
||||
- `X-PIN-Verification-Token: <PIN_ACTION_TOKEN>`
|
||||
- `Content-Type: application/json`
|
||||
|
||||
Minimum required body:
|
||||
- `data.type = contact_<role>_create`
|
||||
- `attributes.role_id`
|
||||
- `attributes.email`
|
||||
- `attributes.first_name`
|
||||
- `attributes.last_name`
|
||||
|
||||
Allowed create types:
|
||||
- `contact_pilot_create`
|
||||
- `contact_doctor_create`
|
||||
- `contact_air_rescuer_create`
|
||||
- `contact_technician_create`
|
||||
- `contact_flight_assistant_create`
|
||||
- `contact_staff_create`
|
||||
|
||||
Rules:
|
||||
- `data.type` must match the role resolved from `attributes.role_id`.
|
||||
- Only one role-specific profile object is allowed, and it must match `data.type`.
|
||||
|
||||
> Note: backend now **automatically sends an invite email** after successful contact creation.
|
||||
|
||||
If PIN token is missing/invalid, API returns `202` with PIN verification required error.
|
||||
|
||||
### Step C - User clicks invite link from email
|
||||
|
||||
Invite email contains a link like:
|
||||
- `https://frontend-domain/set-password?token=<INVITE_TOKEN>`
|
||||
|
||||
This token already carries user identity (email + user id claims) and is single-use.
|
||||
|
||||
### Step D - Frontend submits set password
|
||||
|
||||
Endpoint:
|
||||
- `POST /api/v1/auth/set-password`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_set_password",
|
||||
"attributes": {
|
||||
"token": "<INVITE_TOKEN>",
|
||||
"password": "StrongP@ssw0rd123"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If successful, user can directly log in using `POST /api/v1/auth/login`.
|
||||
|
||||
## 4) Contact Create Payloads by Role
|
||||
|
||||
## Pilot
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_pilot_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_PILOT>",
|
||||
"email": "pilot1@example.com",
|
||||
"username": "pilot1",
|
||||
"first_name": "Marco",
|
||||
"last_name": "Melandri",
|
||||
"mobile_phone": "+628123456789",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"pilot": {
|
||||
"pilot_category": "regular",
|
||||
"short_name": "Marco",
|
||||
"license_no": "PIL-001",
|
||||
"license_issued_at": "2025-01-01T00:00:00Z",
|
||||
"license_expired_at": "2028-01-01T00:00:00Z",
|
||||
"technician_license_no": "TECH-001",
|
||||
"has_ifr_qualification": true,
|
||||
"is_chief_pilot": false,
|
||||
"is_dul": false,
|
||||
"total_flight_minutes": 12000,
|
||||
"responsible_pilot_minutes": 5000,
|
||||
"second_pilot_minutes": 3000,
|
||||
"double_command_minutes": 1000,
|
||||
"flight_instructor_minutes": 500,
|
||||
"night_flight_minutes": 1200,
|
||||
"ifr_flight_minutes": 800,
|
||||
"location": "Jakarta",
|
||||
"postcode": "12950",
|
||||
"street_line": "Jl. Sudirman No. 1",
|
||||
"weight_kg": 72.5,
|
||||
"photo_file_id": "/uploads/contacts/pilot1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Doctor
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_doctor_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_DOCTOR>",
|
||||
"email": "doctor1@example.com",
|
||||
"username": "doctor1",
|
||||
"first_name": "Alicia",
|
||||
"last_name": "Pratama",
|
||||
"mobile_phone": "+628123450001",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"doctor": {
|
||||
"short_name": "Dr. Alicia",
|
||||
"medical_license_no": "MED-DOCTOR-001",
|
||||
"license_issued_at": "2024-01-01T00:00:00Z",
|
||||
"license_expired_at": "2029-01-01T00:00:00Z",
|
||||
"specialization": "Emergency Medicine",
|
||||
"sub_specialization": "Trauma",
|
||||
"is_chief_physician": false,
|
||||
"location": "Jakarta",
|
||||
"postcode": "12950",
|
||||
"street_line": "Jl. Jenderal Sudirman No. 10",
|
||||
"photo_file_id": "/uploads/contacts/doctor1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Air Rescuer
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_air_rescuer_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_AIR_RESCUER>",
|
||||
"email": "airrescuer1@example.com",
|
||||
"username": "airrescuer1",
|
||||
"first_name": "Raka",
|
||||
"last_name": "Wijaya",
|
||||
"mobile_phone": "+628123450002",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"air_rescuer": {
|
||||
"short_name": "Raka",
|
||||
"is_chief_physician_in_charge": false,
|
||||
"responsible_flight_rescuer": false,
|
||||
"location": "Bandung",
|
||||
"postcode": "40115",
|
||||
"street_line": "Jl. Merdeka No. 20",
|
||||
"photo_file_id": "/uploads/contacts/airrescuer1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Technician
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_technician_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_TECHNICIAN>",
|
||||
"email": "tech1@example.com",
|
||||
"username": "tech1",
|
||||
"first_name": "Bima",
|
||||
"last_name": "Nugraha",
|
||||
"mobile_phone": "+628123450003",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"technician": {
|
||||
"short_name": "Bima",
|
||||
"license_no": "TECH-7788",
|
||||
"is_chief_technician": false,
|
||||
"is_responsible_complaints": true,
|
||||
"location": "Surabaya",
|
||||
"postcode": "60231",
|
||||
"street_line": "Jl. Diponegoro No. 30",
|
||||
"photo_file_id": "/uploads/contacts/tech1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Flight Assistant
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_flight_assistant_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_FLIGHT_ASSISTANT>",
|
||||
"email": "fa1@example.com",
|
||||
"username": "fa1",
|
||||
"first_name": "Nadia",
|
||||
"last_name": "Putri",
|
||||
"mobile_phone": "+628123450004",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"flight_assistant": {
|
||||
"short_name": "Nadia",
|
||||
"location": "Yogyakarta",
|
||||
"postcode": "55281",
|
||||
"street_line": "Jl. Kaliurang No. 15",
|
||||
"photo_file_id": "/uploads/contacts/fa1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Staff
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_staff_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_STAFF>",
|
||||
"email": "staff1@example.com",
|
||||
"username": "staff1",
|
||||
"first_name": "Dina",
|
||||
"last_name": "Maharani",
|
||||
"mobile_phone": "+628123450005",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"is_active": true,
|
||||
"staff": {
|
||||
"short_name": "Dina",
|
||||
"role_label": "Operations Staff",
|
||||
"location": "Semarang",
|
||||
"postcode": "50132",
|
||||
"street_line": "Jl. Pandanaran No. 8",
|
||||
"photo_file_id": "/uploads/contacts/staff1.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5) Example Create Contact Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact",
|
||||
"id": "019ce67e-9289-731d-ae4c-0156902aabbc",
|
||||
"attributes": {
|
||||
"role_code": "doctor",
|
||||
"role_name": "doctor",
|
||||
"email": "doctor1@example.com",
|
||||
"first_name": "Alicia",
|
||||
"last_name": "Pratama",
|
||||
"is_active": true,
|
||||
"doctor": {
|
||||
"short_name": "Dr. Alicia",
|
||||
"medical_license_no": "MED-DOCTOR-001"
|
||||
},
|
||||
"invite_sent": true,
|
||||
"invite_error": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If invite sending fails (for example SES/SQS config issue):
|
||||
- contact create is still successful,
|
||||
- `invite_sent = false`,
|
||||
- `invite_error` contains the error message.
|
||||
|
||||
## 6) Error Cases Frontend Should Handle
|
||||
|
||||
1. `202 PIN verification required` on contact create:
|
||||
- call `/auth/pin/verify` again (Step A), then retry create with `X-PIN-Verification-Token`.
|
||||
2. `422 Validation error`:
|
||||
- check payload fields (invalid role_id, invalid email, etc).
|
||||
3. `400` on create:
|
||||
- typically business/constraint conflict.
|
||||
4. `400` on set-password:
|
||||
- token invalid/expired/used or password does not meet rules.
|
||||
|
||||
## 7) Frontend Integration Summary
|
||||
|
||||
1. Admin logs in.
|
||||
2. Fetch `role_id` from roles list.
|
||||
3. Verify PIN (`/auth/pin/verify` with contact-create action ID).
|
||||
4. Create contact (`/contacts/create` + `X-PIN-Verification-Token`).
|
||||
5. Wait for invited user to open invite email.
|
||||
6. Set-password page reads `token` from query param.
|
||||
7. Submit `/auth/set-password` with `{ token, password }`.
|
||||
8. Redirect user to login page.
|
||||
129
docs/contacts_module.md
Normal file
129
docs/contacts_module.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Contacts Module (HEMS)
|
||||
|
||||
## Endpoint listing
|
||||
- `GET /api/v1/contacts?role=pilot`
|
||||
- `GET /api/v1/contacts?role=pilot&pilot_category=freelance`
|
||||
- `GET /api/v1/contacts?role=pilot&pilot_category=dry_lease`
|
||||
- `GET /api/v1/contacts?role=doctor`
|
||||
- `GET /api/v1/contacts?role=air_rescuer`
|
||||
- `GET /api/v1/contacts?role=technician`
|
||||
- `GET /api/v1/contacts?role=flight_assistant`
|
||||
- `GET /api/v1/contacts?role=staff`
|
||||
|
||||
## Sensitive action flow (PIN)
|
||||
1. Client memanggil endpoint sensitif.
|
||||
2. API balas `202` dan memberi tahu `action` PIN yang dibutuhkan.
|
||||
3. Client verifikasi PIN ke `/api/v1/auth/pin/verify`.
|
||||
4. Backend mengembalikan token verifikasi singkat.
|
||||
5. Client kirim ulang request sensitif dengan header `X-PIN-Verification-Token`.
|
||||
|
||||
## Contoh payload
|
||||
|
||||
### 1) Create pilot
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_pilot_create",
|
||||
"attributes": {
|
||||
"role_id": "<ROLE_ID_PILOT>",
|
||||
"email": "pilot1@hems.local",
|
||||
"username": "pilot1",
|
||||
"first_name": "Marco",
|
||||
"last_name": "Pilot",
|
||||
"mobile_phone": "+62811111111",
|
||||
"timezone": "Asia/Jakarta",
|
||||
"pilot": {
|
||||
"pilot_category": "freelance",
|
||||
"short_name": "M. Pilot",
|
||||
"license_no": "LIC-P-1001",
|
||||
"has_ifr_qualification": true,
|
||||
"total_flight_minutes": 12500,
|
||||
"location": "Jakarta"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2) Update doctor
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contact_doctor_update",
|
||||
"attributes": {
|
||||
"is_active": true,
|
||||
"doctor": {
|
||||
"short_name": "dr. Ana",
|
||||
"medical_license_no": "DOC-7788",
|
||||
"specialization": "Anesthesiology",
|
||||
"is_chief_physician": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rule:
|
||||
- `data.type` wajib mengikuti role, mis. `contact_pilot_create`, `contact_air_rescuer_create`, `contact_staff_update`.
|
||||
- Hanya satu object profile role yang boleh dikirim, dan harus sesuai dengan `data.type`.
|
||||
|
||||
### 3) PIN verify sebelum aksi sensitif
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_verify",
|
||||
"attributes": {
|
||||
"pin": "123456",
|
||||
"action": "31f8bb88-a95f-43be-8fd8-129e1355d6a5"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4) Response `202` saat PIN dibutuhkan
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "pin_verification_required",
|
||||
"attributes": {
|
||||
"required": true,
|
||||
"action": "31f8bb88-a95f-43be-8fd8-129e1355d6a5",
|
||||
"method": "pin",
|
||||
"status": 202
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5) List response ringkas
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "contacts",
|
||||
"attributes": {
|
||||
"items": [
|
||||
{
|
||||
"id": "0195f43e-5030-7d2a-8b8a-020063f5ab03",
|
||||
"role_code": "pilot",
|
||||
"role_name": "pilot",
|
||||
"pilot_category": "dry_lease",
|
||||
"username": "pilot_dl_1",
|
||||
"email": "pilot.dl1@hems.local",
|
||||
"first_name": "Rama",
|
||||
"last_name": "Wijaya",
|
||||
"short_name": "R. Wijaya",
|
||||
"license_no": "LIC-P-9988",
|
||||
"location": "Surabaya",
|
||||
"mobile_phone": "+62822222222",
|
||||
"is_active": true
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 1,
|
||||
"limit": 20,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
187
docs/data-migration-wiki-ckeditor.html
Normal file
187
docs/data-migration-wiki-ckeditor.html
Normal file
@@ -0,0 +1,187 @@
|
||||
<h1>Data Migration Wiki</h1>
|
||||
<p>
|
||||
This page is the living technical document for migration scope, decisions, limitations, and open tasks.
|
||||
Update it for every migration wave.
|
||||
</p>
|
||||
|
||||
<h2>Scope</h2>
|
||||
<ul>
|
||||
<li>Database schema migrations in <code>migrations/</code>.</li>
|
||||
<li>Data migration SQL (legacy to current domain model).</li>
|
||||
<li>Non-DB migration concerns: binary files (PDF/images), queue backlog, manual user actions.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Source of Truth</h2>
|
||||
<ul>
|
||||
<li>Versioned DB migrations: <code>migrations/*.sql</code>.</li>
|
||||
<li>Legacy helicopter import flow:
|
||||
<ul>
|
||||
<li><code>migrations/goose/20260414_helicopter_import_staging.sql</code></li>
|
||||
<li><code>migrations/goose/20260415_helicopter_import_apply.sql</code></li>
|
||||
<li><code>migrations/scripts/migration_helicopter.sql</code> (deprecated helper note)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Queue migration notes: <code>docs/queue-sqs-migration.md</code>.</li>
|
||||
<li>File manager APIs and attachment model:
|
||||
<ul>
|
||||
<li><code>docs/file-manager-upload.md</code></li>
|
||||
<li><code>docs/file-manager-attachment.md</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Implemented Data Migrations</h2>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>20260326_move_responsible_flight_rescuer_to_air_rescuer_profiles.sql</strong>
|
||||
<ul>
|
||||
<li>Copies <code>pilot_profiles.responsible_flight_rescuer</code> into <code>air_rescuer_profiles.responsible_flight_rescuer</code>.</li>
|
||||
<li>Drops old column from <code>pilot_profiles</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>20260403_file_manager_file_lifecycle_phase1.sql</strong>
|
||||
<ul>
|
||||
<li>Normalizes legacy file lifecycle states (<code>pending/uploading</code>) into newer states (<code>validated/processing</code>).</li>
|
||||
<li>Carries <code>upload_error</code> into <code>failure_reason</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>20260415_helicopter_import_apply.sql</strong>
|
||||
<ul>
|
||||
<li>Imports rows from <code>helicopter_old_import</code> into <code>helicopters</code>.</li>
|
||||
<li>Maintains old-to-new ID mapping via <code>helicopter_old_id_map</code>.</li>
|
||||
<li>Explicitly non-reversible (<code>Down</code> keeps data as audit trail).</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Known Migration Decisions and Gaps</h2>
|
||||
|
||||
<h3>Confirmed "Will Not Migrate" (Current Decision)</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Legacy helicopter columns intentionally ignored:
|
||||
<ul>
|
||||
<li><code>counter</code></li>
|
||||
<li><code>service</code></li>
|
||||
<li><code>disabled</code></li>
|
||||
<li><code>visible</code></li>
|
||||
</ul>
|
||||
<p>Decision is documented in <code>migrations/scripts/migration_helicopter.sql</code>.</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Confirmed "Not Migrated Yet" (Open)</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Legacy SQS queue backlog
|
||||
<ul>
|
||||
<li>Current worker does not consume old SQS backlog.</li>
|
||||
<li>Required action: drain/discard SQS backlog before/at cutover.</li>
|
||||
<li>Source: <code>docs/queue-sqs-migration.md</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Binary files (PDF/images) into new file manager storage
|
||||
<ul>
|
||||
<li>Current migrations create file manager tables and attachment relations, but no SQL migration imports binary objects or bulk-inserts legacy metadata into <code>file_files</code>/<code>attachments</code>.</li>
|
||||
<li>This means legacy documents may need manual re-upload unless a dedicated import job is created.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Legacy <code>photo_file_id</code> references in contact profile tables
|
||||
<ul>
|
||||
<li>Contact profile schemas still include <code>photo_file_id</code> string columns.</li>
|
||||
<li>No migration currently maps these into <code>attachments</code> + <code>image_attachment_id</code>/<code>foto_attachment_id</code>.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Manual Actions Required at Cutover</h2>
|
||||
<ol>
|
||||
<li>Validate which legacy PDFs/images must remain accessible on day 1.</li>
|
||||
<li>For files without automated import, perform manual re-upload via file manager flow.</li>
|
||||
<li>Re-link uploaded files to business references (attachments) and to entity-level attachment columns where relevant.</li>
|
||||
<li>Obtain client sign-off for any files intentionally left unmigrated.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Migration TODO Register</h2>
|
||||
<table border="1" cellpadding="6" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Item</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Owner</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>MIG-001</td>
|
||||
<td>Decide final strategy for legacy PDFs/images: automated import vs manual re-upload</td>
|
||||
<td>Decision</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>Blocks final cutover checklist</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIG-002</td>
|
||||
<td>Produce inventory of all legacy file references and classify critical/non-critical</td>
|
||||
<td>Analysis</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>Include PDFs + photos</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIG-003</td>
|
||||
<td>If automation chosen, implement importer to populate <code>file_files</code> and <code>attachments</code> safely</td>
|
||||
<td>Engineering</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>Prefer idempotent batch import with audit log</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIG-004</td>
|
||||
<td>Define mapping from legacy contact <code>photo_file_id</code> to new attachment model</td>
|
||||
<td>Engineering</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>Covers pilot/doctor/air rescuer/technician/FA/staff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIG-005</td>
|
||||
<td>Document and execute SQS backlog drain/discard plan before production cutover</td>
|
||||
<td>Ops</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>See queue migration doc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MIG-006</td>
|
||||
<td>Create client-facing exception list for data that cannot/will not be migrated</td>
|
||||
<td>Governance</td>
|
||||
<td>Open</td>
|
||||
<td>TBD</td>
|
||||
<td>Must be approved before go-live</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Cutover Validation Checklist</h2>
|
||||
<ol>
|
||||
<li>Run DB migrations in target environment.</li>
|
||||
<li>Run all required data migration scripts (including helicopter import flow if applicable).</li>
|
||||
<li>Validate row counts and sample records for migrated domains.</li>
|
||||
<li>Validate file accessibility for mandatory records (especially PDFs needed operationally).</li>
|
||||
<li>Validate queue behavior post-cutover (<code>pending</code> to <code>published</code>, no legacy SQS dependency).</li>
|
||||
<li>Record migration result, exceptions, and follow-up tickets on this page.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Change Log</h2>
|
||||
<ul>
|
||||
<li>2026-04-17: Initial wiki created from current migration scripts and docs.</li>
|
||||
<li>2026-04-17: Added CKEditor-ready HTML version.</li>
|
||||
</ul>
|
||||
94
docs/data-migration-wiki.md
Normal file
94
docs/data-migration-wiki.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Data Migration Wiki
|
||||
|
||||
This page is the living technical document for migration scope, decisions, limitations, and open tasks.
|
||||
Update it for every migration wave.
|
||||
For CKEditor copy-paste, use the HTML version: `docs/data-migration-wiki-ckeditor.html`.
|
||||
|
||||
## Scope
|
||||
|
||||
- Database schema migrations in [`migrations/`](../migrations/).
|
||||
- Data migration SQL (legacy to current domain model).
|
||||
- Non-DB migration concerns: binary files (PDF/images), queue backlog, manual user actions.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
- Versioned DB migrations: [`migrations/*.sql`](../migrations/).
|
||||
- Legacy helicopter import flow:
|
||||
- [`migrations/goose/20260414_helicopter_import_staging.sql`](../migrations/goose/20260414_helicopter_import_staging.sql)
|
||||
- [`migrations/goose/20260415_helicopter_import_apply.sql`](../migrations/goose/20260415_helicopter_import_apply.sql)
|
||||
- Deprecated helper note: [`migrations/scripts/migration_helicopter.sql`](../migrations/scripts/migration_helicopter.sql)
|
||||
- Queue migration notes: [`docs/queue-sqs-migration.md`](./queue-sqs-migration.md)
|
||||
- File manager APIs and attachment model:
|
||||
- [`docs/file-manager-upload.md`](./file-manager-upload.md)
|
||||
- [`docs/file-manager-attachment.md`](./file-manager-attachment.md)
|
||||
|
||||
## Implemented Data Migrations
|
||||
|
||||
1. `20260326_move_responsible_flight_rescuer_to_air_rescuer_profiles.sql`
|
||||
- Copies `pilot_profiles.responsible_flight_rescuer` into `air_rescuer_profiles.responsible_flight_rescuer`.
|
||||
- Drops old column from `pilot_profiles`.
|
||||
|
||||
2. `20260403_file_manager_file_lifecycle_phase1.sql`
|
||||
- Normalizes legacy file lifecycle states (`pending/uploading`) into newer states (`validated/processing`) and carries `upload_error` into `failure_reason`.
|
||||
|
||||
3. `20260415_helicopter_import_apply.sql`
|
||||
- Imports rows from `helicopter_old_import` into `helicopters`.
|
||||
- Maintains old-to-new ID mapping via `helicopter_old_id_map`.
|
||||
- Explicitly non-reversible (`Down` keeps data as audit trail).
|
||||
|
||||
## Known Migration Decisions and Gaps
|
||||
|
||||
### Confirmed "Will Not Migrate" (Current Decision)
|
||||
|
||||
1. Legacy helicopter columns intentionally ignored:
|
||||
- `counter`
|
||||
- `service`
|
||||
- `disabled`
|
||||
- `visible`
|
||||
- Decision is documented in [`migrations/scripts/migration_helicopter.sql`](../migrations/scripts/migration_helicopter.sql).
|
||||
|
||||
### Confirmed "Not Migrated Yet" (Open)
|
||||
|
||||
1. Legacy SQS queue backlog
|
||||
- Current worker does not consume old SQS backlog.
|
||||
- Required action: drain/discard SQS backlog before/at cutover.
|
||||
- Source: [`docs/queue-sqs-migration.md`](./queue-sqs-migration.md).
|
||||
|
||||
2. Binary files (PDF/images) into new file manager storage
|
||||
- Current migrations create file manager tables and attachment relations, but no SQL migration imports binary objects or bulk-inserts legacy metadata into `file_files`/`attachments`.
|
||||
- This means legacy documents may need manual re-upload unless a dedicated import job is created.
|
||||
|
||||
3. Legacy `photo_file_id` references in contact profile tables
|
||||
- Contact profile schemas still include `photo_file_id` string columns.
|
||||
- No migration currently maps these into `attachments` + `image_attachment_id`/`foto_attachment_id`.
|
||||
|
||||
## Manual Actions Required at Cutover
|
||||
|
||||
1. Validate which legacy PDFs/images must remain accessible on day 1.
|
||||
2. For files without automated import, perform manual re-upload via file manager flow.
|
||||
3. Re-link uploaded files to business references (attachments) and to entity-level attachment columns where relevant.
|
||||
4. Obtain client sign-off for any files intentionally left unmigrated.
|
||||
|
||||
## Migration TODO Register
|
||||
|
||||
| ID | Item | Type | Status | Owner | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| MIG-001 | Decide final strategy for legacy PDFs/images: automated import vs manual re-upload | Decision | Open | TBD | Blocks final cutover checklist |
|
||||
| MIG-002 | Produce inventory of all legacy file references and classify critical/non-critical | Analysis | Open | TBD | Include PDFs + photos |
|
||||
| MIG-003 | If automation chosen, implement importer to populate `file_files` and `attachments` safely | Engineering | Open | TBD | Prefer idempotent batch import with audit log |
|
||||
| MIG-004 | Define mapping from legacy contact `photo_file_id` to new attachment model | Engineering | Open | TBD | Covers pilot/doctor/air rescuer/technician/FA/staff |
|
||||
| MIG-005 | Document and execute Redis backlog drain/discard plan before production cutover | Ops | Open | TBD | See queue migration doc |
|
||||
| MIG-006 | Create client-facing exception list for data that cannot/will not be migrated | Governance | Open | TBD | Must be approved before go-live |
|
||||
|
||||
## Cutover Validation Checklist
|
||||
|
||||
1. Run DB migrations in target environment.
|
||||
2. Run all required data migration scripts (including helicopter import flow if applicable).
|
||||
3. Validate row counts and sample records for migrated domains.
|
||||
4. Validate file accessibility for mandatory records (especially PDFs needed operationally).
|
||||
5. Validate queue behavior post-cutover (`pending` to `published`, no legacy Redis dependency).
|
||||
6. Record migration result, exceptions, and follow-up tickets in this page.
|
||||
|
||||
## Change Log
|
||||
|
||||
- 2026-04-17: Initial wiki created from current migration scripts and docs.
|
||||
38390
docs/docs.go
Normal file
38390
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
17
docs/docs_test.go
Normal file
17
docs/docs_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
func TestInitRegistersSwaggerSpec(t *testing.T) {
|
||||
registered := swag.GetSwagger(SwaggerInfo.InstanceName())
|
||||
if registered == nil {
|
||||
t.Fatalf("swagger spec %q was not registered", SwaggerInfo.InstanceName())
|
||||
}
|
||||
if registered.ReadDoc() == "" {
|
||||
t.Fatalf("registered swagger doc is empty")
|
||||
}
|
||||
}
|
||||
74
docs/duty-roster-migrations.md
Normal file
74
docs/duty-roster-migrations.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Duty Roster Migrations
|
||||
|
||||
Catatan eksekusi 6 file migrasi duty roster di [`migrations/`](../migrations/).
|
||||
|
||||
## Konfigurasi Tool Migrasi
|
||||
|
||||
Tool migrasi yang dipakai project ini adalah `goose` ([Makefile:5](../Makefile#L5)) dengan direktori target:
|
||||
|
||||
```makefile
|
||||
GOOSE_DIR ?= migrations/goose
|
||||
```
|
||||
|
||||
**Konsekuensi:** `make migrate-up` hanya menjalankan file di [`migrations/goose/`](../migrations/goose/). Semua file `.sql` di root [`migrations/`](../migrations/) — termasuk 6 file duty roster — **tidak otomatis dieksekusi**. Mereka berfungsi sebagai script SQL ad-hoc / log historis dan harus dijalankan manual (`mysql < file.sql`) atau dipindahkan ke `migrations/goose/` dengan format `-- +goose Up` / `-- +goose Down`.
|
||||
|
||||
## Urutan Eksekusi (by Timestamp)
|
||||
|
||||
Goose mengurutkan file by filename ascending. Untuk dua file dengan timestamp sama (`20260502`), urutannya alfabetis: `duty` < `purge`.
|
||||
|
||||
```
|
||||
1. 20260408_hems_duty_roster_crews_confirm_at.sql
|
||||
2. 20260429_rename_duty_roster_tables.sql
|
||||
3. 20260430_normalize_duty_roster_legacy.sql
|
||||
4. 20260502_duty_roster_permissions_unification.sql
|
||||
5. 20260502_purge_legacy_hems_duty_roster_permissions.sql
|
||||
6. 20260504_duty_roster_drop_unique_base_date.sql
|
||||
```
|
||||
|
||||
## Efek per File
|
||||
|
||||
| # | File | Efek di DB |
|
||||
|---|---|---|
|
||||
| 1 | [`20260408_hems_duty_roster_crews_confirm_at.sql`](../migrations/20260408_hems_duty_roster_crews_confirm_at.sql) | `ALTER TABLE hems_duty_roster_crews ADD COLUMN confirm_at DATETIME NULL` (nama tabel lama). Idempoten via `IF NOT EXISTS`. |
|
||||
| 2 | [`20260429_rename_duty_roster_tables.sql`](../migrations/20260429_rename_duty_roster_tables.sql) | `RENAME TABLE hems_duty_rosters → duty_rosters` dan `hems_duty_roster_crews → duty_roster_crews`. Pakai guard cek `information_schema.TABLES` jadi idempoten. |
|
||||
| 3 | [`20260430_normalize_duty_roster_legacy.sql`](../migrations/20260430_normalize_duty_roster_legacy.sql) | (a) Rename tabel **lagi** dengan guard sama (no-op kalau #2 sukses). (b) Rename kolom `hems_base_id → base_id` di `duty_rosters`. (c) Ensure `confirm_at` ada di `duty_roster_crews` baru (dan di tabel lama jika masih ada). (d) Rewire FK `roster_crews.roster_id` & `duty_roster_other_persons.roster_id` supaya reference `duty_rosters` (bukan `hems_duty_rosters`). |
|
||||
| 4 | [`20260502_duty_roster_permissions_unification.sql`](../migrations/20260502_duty_roster_permissions_unification.sql) | Insert 4 permission canonical: `duty_roster.{create,read,update,delete}` (skip via `WHERE NOT EXISTS`). Grant ke role yang punya legacy `hems_duty_roster.*` ke canonical baru, anti-duplikasi via `LEFT JOIN ... NOT EXISTS`. **Tidak menghapus legacy.** |
|
||||
| 5 | [`20260502_purge_legacy_hems_duty_roster_permissions.sql`](../migrations/20260502_purge_legacy_hems_duty_roster_permissions.sql) | (a) Re-insert canonical (no-op jika #4 sudah jalan). (b) Re-grant (no-op). (c) `DELETE FROM role_permissions WHERE permission.key IN (legacy)`. (d) `DELETE FROM permissions WHERE key IN (legacy)`. |
|
||||
| 6 | [`20260504_duty_roster_drop_unique_base_date.sql`](../migrations/20260504_duty_roster_drop_unique_base_date.sql) | `DROP INDEX idx_roster_base_date` (sebelumnya unique). `CREATE INDEX idx_roster_base_date ON duty_rosters(base_id, duty_date)` non-unique. **Konsekuensi aplikasi**: schema tidak lagi mencegah multiple roster di pasangan `(base_id, duty_date)` yang sama — kode handler harus mendukung skenario ini (lihat [`ensureRosterDates`](../internal/transport/http/handlers/duty_roster_handler.go) yang sebelumnya dedup-by-date). |
|
||||
|
||||
## Overlap & Redundansi
|
||||
|
||||
- **#2 ⊂ #3** — File `0430_normalize_legacy` sudah include semua rename dari `0429` dengan guard yang sama. File `0429` secara fungsional redundant; akan no-op jika dijalankan setelah `0430`.
|
||||
- **#4 ⊂ #5** — File `0502_purge_legacy` adalah superset dari `0502_unification`. Insert + grant identik, lalu tambah `DELETE`. File `0502_unification` redundant.
|
||||
|
||||
## Idempotensi Re-run
|
||||
|
||||
| File | Idempoten? | Catatan |
|
||||
|---|---|---|
|
||||
| #1 | Ya | `ADD COLUMN IF NOT EXISTS` |
|
||||
| #2 | Ya | Guard via `information_schema.TABLES` |
|
||||
| #3 | Ya | Setiap `ALTER` dibungkus prepared statement dengan kondisi |
|
||||
| #4 | Ya | `WHERE NOT EXISTS` untuk insert + `LEFT JOIN ... IS NULL` untuk grant |
|
||||
| #5 | Ya | Sama dengan #4, ditambah `DELETE` (idempoten karena hanya hapus key legacy) |
|
||||
| **#6** | **TIDAK** | `DROP INDEX idx_roster_base_date` tanpa guard. Re-run setelah index sudah di-drop + recreate (sebagai non-unique) akan **gagal** karena nama indexnya sama. Mestinya pakai prepared statement guard yang cek `information_schema.STATISTICS`. |
|
||||
|
||||
## Versi Minimal yang Setara
|
||||
|
||||
Dengan urutan dijalankan penuh, efek akhirnya cukup dicapai dengan 4 file:
|
||||
|
||||
```
|
||||
0408_..._confirm_at (kompat untuk env yang masih punya hems_duty_*)
|
||||
0430_normalize_legacy (rename tabel + kolom + FK + confirm_at — superset dari 0429)
|
||||
0502_purge_legacy (canonical perm + delete legacy — superset dari 0502_unification)
|
||||
0504_drop_unique_base_date (butuh tambah guard agar idempoten)
|
||||
```
|
||||
|
||||
Untuk env yang sudah pasti **fresh** (tanpa sisa `hems_duty_*` apa pun), kelima file di atas bisa digabung jadi satu file goose-format dengan branch yang relevan saja. **Sebelum melakukan ini, pastikan tidak ada DB target yang masih nyangkut di mid-state** — file individual lebih aman selama transisi.
|
||||
|
||||
## Bug yang Pernah Ditemukan Akibat #6
|
||||
|
||||
Setelah `idx_roster_base_date` jadi non-unique, beberapa baris kode aplikasi yang asumsi unique (`base_id`, `duty_date`) jadi salah:
|
||||
|
||||
- [`ensureRosterDates`](../internal/transport/http/handlers/duty_roster_handler.go) di handler GET `/duty-roster/get-all` & `/get-all/dt` sebelumnya pakai `map[string]R` keyed by date — multiple roster di tanggal sama di-overwrite, hanya 1 yang muncul. Sudah diperbaiki pakai `map[string][]R` (multi-value).
|
||||
|
||||
Pastikan setiap kode baru yang query/iterasi roster per `(base_id, duty_date)` menyadari bahwa kombinasi ini tidak lagi unique.
|
||||
735
docs/error-catalog.md
Normal file
735
docs/error-catalog.md
Normal file
@@ -0,0 +1,735 @@
|
||||
# Error Catalog
|
||||
|
||||
Dokumen ini berisi daftar lengkap kontrak error yang dikembalikan backend (`apperrorsx`). Single source of truth ada pada [internal/shared/pkg/apperrorsx/codes_defs.go](../internal/shared/pkg/apperrorsx/codes_defs.go).
|
||||
|
||||
|
||||
## Error Code Format
|
||||
|
||||
Format `error_code`:
|
||||
|
||||
```text
|
||||
[HTTP_STATUS(3)][MODULE_CODE(2)][CASE_CODE(2)]
|
||||
```
|
||||
|
||||
Contoh:
|
||||
|
||||
- `4010202` → status `401`, module `02` (PIN), case `02` (PIN blocked)
|
||||
- `4040302` → status `404`, module `03` (HELICOPTER), case `02` (Helicopter not found)
|
||||
|
||||
## Module Code
|
||||
|
||||
| Code | Module | Konstanta |
|
||||
|---:|---|---|
|
||||
| `00` | General | `ModuleGeneral` |
|
||||
| `01` | Auth | `ModuleAuth` |
|
||||
| `02` | PIN | `ModulePIN` |
|
||||
| `03` | Helicopter | `ModuleHelicopter` |
|
||||
| `04` | User | `ModuleUser` |
|
||||
| `05` | Master Data | `ModuleMasterData` |
|
||||
| `06` | Duty Roster | `ModuleDutyRoster` |
|
||||
| `07` | Flight | `ModuleFlight` |
|
||||
| `08` | Contact | `ModuleContact` |
|
||||
| `09` | HEMS / Mission Operations | `ModuleMissionOps` |
|
||||
| `10` | Facility | `ModuleFacility` |
|
||||
| `11` | No ICAO Code | `ModuleHospital` |
|
||||
| `12` | Federal State | `ModuleFederalState` |
|
||||
| `13` | Nationality | `ModuleNationality` |
|
||||
| `14` | ICAO | `ModuleICAO` |
|
||||
| `15` | Country | `ModuleLand` |
|
||||
| `16` | Base | `ModuleBase` |
|
||||
| `17` | Health Insurance Companies | `ModuleHealthInsuranceCompanies` |
|
||||
| `18` | Vocation | `ModuleVocation` |
|
||||
| `19` | Medicine | `ModuleMedicine` |
|
||||
| `20` | Master Settings | `ModuleMasterSettings` |
|
||||
| `21` | File Manager | `ModuleFileManager` |
|
||||
| `22` | OPC Data | `ModuleOPCData` |
|
||||
| `23` | Patient Data | `ModulePatientData` |
|
||||
| `24` | Insurance Patient Data | `ModuleInsurancePatientData` |
|
||||
| `25` | Role | `ModuleRole` |
|
||||
| `26` | Flight Data | `ModuleFlightData` |
|
||||
| `27` | Air Rescuer Checklist | `ModuleAirRescuerChecklist` |
|
||||
| `28` | Branding | `ModuleBranding` |
|
||||
| `29` | DUL | `ModuleDUL` |
|
||||
| `37` | After Flight Inspection | `ModuleAfterFlightInspection` |
|
||||
| `30` | Takeover | `ModuleTakeover` |
|
||||
| `31` | Fleet Status | `ModuleFleetStatus` |
|
||||
|
||||
## Catalog
|
||||
|
||||
### General (Module `00`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `INVALID_REQUEST` | `4000001` | 400 | Bad Request | invalid request |
|
||||
| `DATA_NOT_FOUND` | `4040002` | 404 | Not found | data not found |
|
||||
| `VALIDATION_ERROR` | `4220003` | 422 | Validation error | validation error |
|
||||
| `BUSINESS_RULE_FAILED` | `4220004` | 422 | Business rule failed | business rule validation failed |
|
||||
| `INTERNAL_SERVER_ERROR` | `5000005` | 500 | Internal server error | internal server error |
|
||||
|
||||
### Auth (Module `01`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `SESSION_EXPIRED` | `4010101` | 401 | Unauthorized | session expired |
|
||||
| `TOKEN_INVALID` | `4010102` | 401 | Unauthorized | token is invalid |
|
||||
| `USER_UNAUTHORIZED` | `4010103` | 401 | Unauthorized | user is unauthorized |
|
||||
| `FORBIDDEN_ACCESS` | `4030104` | 403 | Forbidden | forbidden access |
|
||||
| `AUTH_INVALID_JSON` | `4000105` | 400 | Bad Request | invalid json |
|
||||
| `AUTH_REGISTRATION_DISABLED` | `4030106` | 403 | Forbidden | registration is disabled |
|
||||
| `AUTH_ROLE_NOT_FOUND` | `4000107` | 400 | Bad Request | public registration role not found |
|
||||
| `AUTH_REGISTRATION_FAILED` | `4000108` | 400 | Bad Request | registration failed |
|
||||
| `AUTH_INVITE_FAILED` | `4000109` | 400 | Bad Request | invite failed |
|
||||
| `AUTH_SET_PASSWORD_FAILED` | `4000110` | 400 | Bad Request | set password failed |
|
||||
| `AUTH_SET_PASSWORD_TOKEN_INVALID` | `4000111` | 400 | Bad Request | set password token invalid |
|
||||
| `AUTH_USERNAME_ALREADY_REGISTERED` | `4000112` | 400 | Bad Request | username already registered |
|
||||
| `AUTH_RESET_PASSWORD_INVALID_LINK` | `4000113` | 400 | Bad Request | reset password link invalid |
|
||||
| `AUTH_RESET_PASSWORD_FAILED` | `5000114` | 500 | Internal server error | reset password failed |
|
||||
|
||||
### PIN (Module `02`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `PIN_INCORRECT` | `4010201` | 401 | Unauthorized | invalid security pin |
|
||||
| `PIN_BLOCKED` | `4230202` | 423 | Locked | PIN has been blocked |
|
||||
| `PIN_ATTEMPT_LIMIT_REACHED` | `4290203` | 429 | Too many requests | PIN attempt limit reached |
|
||||
| `PIN_NOT_SET` | `4090204` | 409 | PIN not configured | security PIN is not set |
|
||||
| `PIN_NOT_BLOCKED` | `4090207` | 409 | PIN not blocked | security PIN is not blocked |
|
||||
| `PIN_VALIDATION_FAILED` | `4220205` | 422 | Validation error | PIN validation failed |
|
||||
| `PIN_VERIFICATION_REQUIRED` | `2020206` | 202 | PIN verification required | valid PIN action token is required |
|
||||
|
||||
### Helicopter (Module `03`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `HELICOPTER_DUPLICATE` | `4090301` | 409 | Conflict | helicopter identifier already exists |
|
||||
| `HELICOPTER_NOT_FOUND` | `4040302` | 404 | Not found | helicopter not found |
|
||||
| `HELICOPTER_RELATION_DELETE_ERROR` | `4090303` | 409 | Conflict | cannot delete helicopter due to active relations |
|
||||
| `HELICOPTER_INVALID_PAYLOAD` | `4220304` | 422 | Validation error | invalid helicopter payload |
|
||||
| `HELICOPTER_INVALID_JSON` | `4000305` | 400 | Bad Request | invalid json payload |
|
||||
| `HELICOPTER_INVALID_ID` | `4220306` | 422 | Validation error | helicopter id is invalid |
|
||||
| `HELICOPTER_ID_REQUIRED` | `4220307` | 422 | Validation error | helicopter id is required |
|
||||
| `HELICOPTER_ID_MISMATCH` | `4220308` | 422 | Validation error | helicopter id mismatch |
|
||||
| `HELICOPTER_ATTRIBUTES_REQUIRED` | `4220309` | 422 | Validation error | at least one attribute must be provided |
|
||||
| `HELICOPTER_CREATE_FAILED` | `4000310` | 400 | Bad Request | failed to create helicopter |
|
||||
| `HELICOPTER_UPDATE_FAILED` | `4000311` | 400 | Bad Request | failed to update helicopter |
|
||||
| `HELICOPTER_DELETE_FAILED` | `4000312` | 400 | Bad Request | failed to delete helicopter |
|
||||
| `HELICOPTER_GENERATE_FAILED` | `4000313` | 400 | Bad Request | failed to generate helicopter report number |
|
||||
| `HELICOPTER_LIST_FAILED` | `4000314` | 400 | Bad Request | failed to list helicopter data |
|
||||
| `HELICOPTER_PIN_VERIFICATION_REQUIRED` | `2020315` | 202 | PIN verification required | valid PIN action token is required |
|
||||
| `HELICOPTER_UNAUTHORIZED` | `4010316` | 401 | Unauthorized | helicopter action unauthorized |
|
||||
| `HELICOPTER_INVALID_TYPE` | `4220317` | 422 | Validation error | helicopter type is invalid |
|
||||
| `HELICOPTER_FILE_NOT_FOUND` | `4040318` | 404 | Not found | helicopter file not found |
|
||||
| `HELICOPTER_FILE_INVALID_PAYLOAD` | `4220319` | 422 | Validation error | invalid helicopter file payload |
|
||||
| `HELICOPTER_FILE_INVALID_JSON` | `4000320` | 400 | Bad Request | invalid json payload |
|
||||
| `HELICOPTER_FILE_INVALID_UUID` | `4220321` | 422 | Validation error | helicopter file uuid is invalid |
|
||||
| `HELICOPTER_FILE_SECTION_INVALID` | `4220322` | 422 | Validation error | section is invalid |
|
||||
| `HELICOPTER_FILE_FILE_NAME_REQUIRED` | `4220323` | 422 | Validation error | file_name is required |
|
||||
| `HELICOPTER_FILE_CREATE_FAILED` | `4000324` | 400 | Bad Request | failed to create helicopter file |
|
||||
| `HELICOPTER_FILE_UPDATE_FAILED` | `4000325` | 400 | Bad Request | failed to update helicopter file |
|
||||
| `HELICOPTER_FILE_DELETE_FAILED` | `4000326` | 400 | Bad Request | failed to delete helicopter file |
|
||||
| `HELICOPTER_FILE_GET_FAILED` | `4000327` | 400 | Bad Request | failed to get helicopter file |
|
||||
| `HELICOPTER_FILE_LIST_FAILED` | `4000328` | 400 | Bad Request | failed to list helicopter file data |
|
||||
|
||||
### User (Module `04`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `USER_NOT_FOUND` | `4040401` | 404 | Not found | user not found |
|
||||
| `USER_INVALID_PAYLOAD` | `4220402` | 422 | Validation error | invalid user payload |
|
||||
| `USER_INVALID_JSON` | `4000403` | 400 | Bad Request | invalid json payload |
|
||||
| `USER_INVALID_UUID` | `4220404` | 422 | Validation error | user uuid is invalid |
|
||||
| `USER_VALIDATION_FAILED` | `4220405` | 422 | Validation error | user validation failed |
|
||||
| `USER_CREATE_FAILED` | `4000406` | 400 | Bad Request | failed to create user |
|
||||
| `USER_UPDATE_FAILED` | `4000407` | 400 | Bad Request | failed to update user |
|
||||
| `USER_DELETE_FAILED` | `4000408` | 400 | Bad Request | failed to delete user |
|
||||
| `USER_LIST_FAILED` | `4000409` | 400 | Bad Request | failed to list users |
|
||||
| `USER_TIMEZONE_INVALID` | `4220410` | 422 | Validation error | timezone is invalid |
|
||||
| `USER_ROLE_IDS_INVALID` | `4220411` | 422 | Validation error | role_ids contains invalid uuid |
|
||||
| `USER_ROLE_IDS_REQUIRED` | `4220412` | 422 | Validation error | role_ids must contain at least one role |
|
||||
| `USER_PRIMARY_ROLE_INVALID` | `4220413` | 422 | Validation error | primary role must be included in role_ids |
|
||||
|
||||
### Master Data (Module `05`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `MASTER_DATA_NOT_FOUND` | `4040501` | 404 | Not found | master data not found |
|
||||
| `MASTER_DATA_INVALID_PAYLOAD` | `4220502` | 422 | Validation error | invalid master data payload |
|
||||
| `MASTER_DATA_INVALID_JSON` | `4000503` | 400 | Bad Request | invalid json payload |
|
||||
| `MASTER_DATA_VALIDATION_FAILED` | `4220504` | 422 | Validation error | master data validation failed |
|
||||
| `MASTER_DATA_CREATE_FAILED` | `4000505` | 400 | Bad Request | failed to create master data |
|
||||
| `MASTER_DATA_UPDATE_FAILED` | `4000506` | 400 | Bad Request | failed to update master data |
|
||||
| `MASTER_DATA_DELETE_FAILED` | `4000507` | 400 | Bad Request | failed to delete master data |
|
||||
| `MASTER_DATA_LIST_FAILED` | `4000508` | 400 | Bad Request | failed to list master data |
|
||||
|
||||
### Duty Roster (Module `06`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `DUTY_ROSTER_NOT_FOUND` | `4040601` | 404 | Not found | duty roster not found |
|
||||
| `DUTY_ROSTER_INVALID_PAYLOAD` | `4220602` | 422 | Validation error | invalid duty roster payload |
|
||||
| `DUTY_ROSTER_INVALID_JSON` | `4000603` | 400 | Bad Request | invalid json payload |
|
||||
| `DUTY_ROSTER_INVALID_UUID` | `4220604` | 422 | Validation error | duty roster uuid is invalid |
|
||||
| `DUTY_ROSTER_VALIDATION_FAILED` | `4220605` | 422 | Validation error | duty roster validation failed |
|
||||
| `DUTY_ROSTER_CONFLICT` | `4090606` | 409 | Conflict | duty roster conflict |
|
||||
| `DUTY_ROSTER_CREATE_FAILED` | `4000607` | 400 | Bad Request | failed to create duty roster |
|
||||
| `DUTY_ROSTER_UPDATE_FAILED` | `4000608` | 400 | Bad Request | failed to update duty roster |
|
||||
| `DUTY_ROSTER_DELETE_FAILED` | `4000609` | 400 | Bad Request | failed to delete duty roster |
|
||||
| `DUTY_ROSTER_LIST_FAILED` | `4000610` | 400 | Bad Request | failed to list duty roster data |
|
||||
| `DUTY_ROSTER_UNAUTHORIZED` | `4010611` | 401 | Unauthorized | duty roster unauthorized |
|
||||
| `DUTY_ROSTER_BASE_TYPE_INVALID` | `4220612` | 422 | Validation error | duty roster base type is invalid |
|
||||
| `DUTY_ROSTER_RELATION_DELETE_ERROR` | `4090613` | 409 | Conflict | cannot delete duty roster due to active relations |
|
||||
|
||||
### DUL (Module `29`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `DUL_NOT_FOUND` | `4042901` | 404 | Not found | dul not found |
|
||||
| `DUL_INVALID_PAYLOAD` | `4222902` | 422 | Validation error | invalid dul payload |
|
||||
| `DUL_INVALID_JSON` | `4002903` | 400 | Bad Request | invalid json payload |
|
||||
| `DUL_INVALID_UUID` | `4222904` | 422 | Validation error | dul uuid is invalid |
|
||||
| `DUL_VALIDATION_FAILED` | `4222905` | 422 | Validation error | dul validation failed |
|
||||
| `DUL_CREATE_FAILED` | `4002906` | 400 | Bad Request | failed to create dul |
|
||||
| `DUL_UPDATE_FAILED` | `4002907` | 400 | Bad Request | failed to update dul |
|
||||
| `DUL_DELETE_FAILED` | `4002908` | 400 | Bad Request | failed to delete dul |
|
||||
| `DUL_LIST_FAILED` | `4002909` | 400 | Bad Request | failed to list dul |
|
||||
| `DUL_DATE_INVALID` | `4222910` | 422 | Validation error | date must be YYYY-MM-DD |
|
||||
|
||||
### Takeover (Module `30`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `TAKEOVER_NOT_FOUND` | `4043001` | 404 | Not found | takeover not found |
|
||||
| `TAKEOVER_INVALID_PAYLOAD` | `4223002` | 422 | Validation error | invalid takeover payload |
|
||||
| `TAKEOVER_INVALID_JSON` | `4003003` | 400 | Bad Request | invalid json payload |
|
||||
| `TAKEOVER_INVALID_UUID` | `4223004` | 422 | Validation error | takeover uuid is invalid |
|
||||
| `TAKEOVER_ID_REQUIRED` | `4223005` | 422 | Validation error | id is required |
|
||||
| `TAKEOVER_ID_MISMATCH` | `4223006` | 422 | Validation error | id does not match path uuid |
|
||||
| `TAKEOVER_FILE_SECTION_INVALID` | `4223007` | 422 | Validation error | helicopter_file_id does not belong to selected helicopter/section |
|
||||
| `TAKEOVER_CREATE_FAILED` | `4003007` | 400 | Bad Request | failed to create takeover |
|
||||
| `TAKEOVER_UPDATE_FAILED` | `4003008` | 400 | Bad Request | failed to update takeover |
|
||||
| `TAKEOVER_DELETE_FAILED` | `4003009` | 400 | Bad Request | failed to delete takeover |
|
||||
| `TAKEOVER_GET_FAILED` | `4003010` | 400 | Bad Request | failed to get takeover |
|
||||
| `TAKEOVER_LIST_FAILED` | `4003011` | 400 | Bad Request | failed to list takeover data |
|
||||
| `TAKEOVER_HELICOPTER_IN_USE` | `4093013` | 409 | Conflict | helicopter is already booked and cannot be used |
|
||||
|
||||
### Fleet Status (Module `31`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FLEET_STATUS_NOT_FOUND` | `4043101` | 404 | Not found | fleet status not found |
|
||||
| `FLEET_STATUS_INVALID_PAYLOAD` | `4223102` | 422 | Validation error | invalid fleet status payload |
|
||||
| `FLEET_STATUS_INVALID_JSON` | `4003103` | 400 | Bad Request | invalid json payload |
|
||||
| `FLEET_STATUS_INVALID_ID` | `4223104` | 422 | Validation error | fleet status id is invalid |
|
||||
| `FLEET_STATUS_ID_REQUIRED` | `4223105` | 422 | Validation error | id is required |
|
||||
| `FLEET_STATUS_ID_MISMATCH` | `4223106` | 422 | Validation error | id does not match path id |
|
||||
| `FLEET_STATUS_HELICOPTER_REQUIRED` | `4223107` | 422 | Validation error | helicopter_id is required |
|
||||
| `FLEET_STATUS_HELICOPTER_INVALID` | `4223108` | 422 | Validation error | referenced helicopter does not exist |
|
||||
| `FLEET_STATUS_DUPLICATE` | `4093109` | 409 | Conflict | fleet status already exists |
|
||||
| `FLEET_STATUS_RELATION_DELETE_ERROR` | `4093110` | 409 | Conflict | cannot delete fleet status due to active relations |
|
||||
| `FLEET_STATUS_CREATE_FAILED` | `4003111` | 400 | Bad Request | failed to create fleet status |
|
||||
| `FLEET_STATUS_UPDATE_FAILED` | `4003112` | 400 | Bad Request | failed to update fleet status |
|
||||
| `FLEET_STATUS_DELETE_FAILED` | `4003113` | 400 | Bad Request | failed to delete fleet status |
|
||||
| `FLEET_STATUS_LIST_FAILED` | `4003114` | 400 | Bad Request | failed to list fleet status data |
|
||||
| `FLEET_STATUS_MARK_SERVICED_FAILED` | `4003115` | 400 | Bad Request | failed to mark fleet status serviced |
|
||||
|
||||
### Flight (Module `07`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FLIGHT_NOT_FOUND` | `4040701` | 404 | Not found | flight not found |
|
||||
| `FLIGHT_INVALID_PAYLOAD` | `4220702` | 422 | Validation error | invalid flight payload |
|
||||
| `FLIGHT_INVALID_JSON` | `4000703` | 400 | Bad Request | invalid json payload |
|
||||
| `FLIGHT_INVALID_UUID` | `4220704` | 422 | Validation error | flight uuid is invalid |
|
||||
| `FLIGHT_VALIDATION_FAILED` | `4220705` | 422 | Validation error | flight validation failed |
|
||||
| `FLIGHT_CREATE_FAILED` | `4000706` | 400 | Bad Request | failed to create flight |
|
||||
| `FLIGHT_GET_FAILED` | `4000707` | 400 | Bad Request | failed to get flight |
|
||||
| `FLIGHT_LIST_FAILED` | `4000708` | 400 | Bad Request | failed to list flight data |
|
||||
| `FLIGHT_UNAUTHORIZED` | `4010709` | 401 | Unauthorized | flight action unauthorized |
|
||||
| `FLIGHT_DATE_INVALID` | `4220710` | 422 | Validation error | date must be YYYY-MM-DD |
|
||||
|
||||
### Contact (Module `08`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `CONTACT_NOT_FOUND` | `4040801` | 404 | Not found | contact not found |
|
||||
| `CONTACT_INVALID_PAYLOAD` | `4220802` | 422 | Validation error | invalid contact payload |
|
||||
| `CONTACT_INVALID_JSON` | `4000803` | 400 | Bad Request | invalid json payload |
|
||||
| `CONTACT_INVALID_USER_ID` | `4220804` | 422 | Validation error | user_id is invalid uuid |
|
||||
| `CONTACT_VALIDATION_FAILED` | `4220805` | 422 | Validation error | contact validation failed |
|
||||
| `CONTACT_CREATE_FAILED` | `4000806` | 400 | Bad Request | failed to create contact |
|
||||
| `CONTACT_GET_FAILED` | `4000807` | 400 | Bad Request | failed to get contact |
|
||||
| `CONTACT_UPDATE_FAILED` | `4000808` | 400 | Bad Request | failed to update contact |
|
||||
| `CONTACT_USERNAME_CONFLICT` | `4090809` | 409 | Conflict | username already exists |
|
||||
| `CONTACT_DELETE_FAILED` | `4000810` | 400 | Bad Request | failed to delete contact |
|
||||
| `CONTACT_UPDATE_STATUS_FAILED` | `4000811` | 400 | Bad Request | failed to update contact status |
|
||||
| `CONTACT_UPDATE_ROLE_FAILED` | `4000812` | 400 | Bad Request | failed to update contact role |
|
||||
| `CONTACT_UPDATE_PILOT_CATEGORY_FAILED` | `4000813` | 400 | Bad Request | failed to update contact pilot category |
|
||||
| `CONTACT_UPDATE_LICENSE_FAILED` | `4000814` | 400 | Bad Request | failed to update contact license |
|
||||
| `CONTACT_UPDATE_CHIEF_FLAGS_FAILED` | `4000815` | 400 | Bad Request | failed to update contact chief flags |
|
||||
| `CONTACT_UPDATE_RESPONSIBLE_COMPLAINTS_FAILED` | `4000816` | 400 | Bad Request | failed to update contact responsible complaints |
|
||||
| `CONTACT_BULK_UPDATE_FAILED` | `4000817` | 400 | Bad Request | failed to bulk update contacts |
|
||||
| `CONTACT_LIST_FAILED` | `4000818` | 400 | Bad Request | failed to list contacts |
|
||||
|
||||
### HEMS Operation / Mission Ops (Module `09`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `HEMS_OPERATION_NOT_FOUND` | `4040901` | 404 | Not found | hems operation not found |
|
||||
| `HEMS_OPERATION_INVALID_PAYLOAD` | `4220902` | 422 | Validation error | invalid hems operation payload |
|
||||
| `HEMS_OPERATION_INVALID_JSON` | `4000903` | 400 | Bad Request | invalid json payload |
|
||||
| `HEMS_OPERATION_INVALID_UUID` | `4220904` | 422 | Validation error | hems operation uuid is invalid |
|
||||
| `HEMS_OPERATION_ATTRIBUTES_REQUIRED` | `4220905` | 422 | Validation error | at least one hems operation attribute must be provided |
|
||||
| `HEMS_OPERATION_DATE_INVALID_RFC3339` | `4220906` | 422 | Validation error | date must be in RFC3339 format |
|
||||
| `HEMS_OPERATION_RELATION_REFERENCE_INVALID` | `4220907` | 422 | Validation error | relation reference is invalid |
|
||||
| `HEMS_OPERATION_CREATE_FAILED` | `4000908` | 400 | Bad Request | failed to create hems operation |
|
||||
| `HEMS_OPERATION_UPDATE_FAILED` | `4000909` | 400 | Bad Request | failed to update hems operation |
|
||||
| `HEMS_OPERATION_DELETE_FAILED` | `4000910` | 400 | Bad Request | failed to delete hems operation |
|
||||
| `HEMS_OPERATION_LIST_FAILED` | `4000911` | 400 | Bad Request | failed to list hems operation |
|
||||
|
||||
### Forces Present (Module `09`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FORCES_PRESENT_NOT_FOUND` | `4040912` | 404 | Not found | forces present not found |
|
||||
| `FORCES_PRESENT_RELATION_DELETE_ERROR` | `4090913` | 409 | Conflict | cannot delete forces present due to active relations |
|
||||
| `FORCES_PRESENT_INVALID_PAYLOAD` | `4220914` | 422 | Validation error | invalid forces present payload |
|
||||
| `FORCES_PRESENT_INVALID_JSON` | `4000915` | 400 | Bad Request | invalid json payload |
|
||||
| `FORCES_PRESENT_INVALID_UUID` | `4220916` | 422 | Validation error | forces present uuid is invalid |
|
||||
| `FORCES_PRESENT_ID_REQUIRED` | `4220917` | 422 | Validation error | forces present id is required |
|
||||
| `FORCES_PRESENT_ID_MISMATCH` | `4220918` | 422 | Validation error | forces present id mismatch |
|
||||
| `FORCES_PRESENT_ATTRIBUTES_REQUIRED` | `4220919` | 422 | Validation error | at least one forces present attribute must be provided |
|
||||
| `FORCES_PRESENT_NAME_REQUIRED` | `4220920` | 422 | Validation error | forces present name is required |
|
||||
| `FORCES_PRESENT_CREATE_FAILED` | `4000921` | 400 | Bad Request | failed to create forces present |
|
||||
| `FORCES_PRESENT_UPDATE_FAILED` | `4000922` | 400 | Bad Request | failed to update forces present |
|
||||
| `FORCES_PRESENT_DELETE_FAILED` | `4000923` | 400 | Bad Request | failed to delete forces present |
|
||||
| `FORCES_PRESENT_LIST_FAILED` | `4000924` | 400 | Bad Request | failed to list forces present data |
|
||||
|
||||
### Mission (Module `09`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `MISSION_NOT_FOUND` | `4040925` | 404 | Not found | mission not found |
|
||||
| `MISSION_INVALID_PAYLOAD` | `4220926` | 422 | Validation error | invalid mission payload |
|
||||
| `MISSION_INVALID_JSON` | `4000927` | 400 | Bad Request | invalid json payload |
|
||||
| `MISSION_INVALID_FLIGHT_UUID` | `4220928` | 422 | Validation error | flight uuid is invalid |
|
||||
| `MISSION_FLIGHT_ID_REQUIRED` | `4220929` | 422 | Validation error | flight_id is required |
|
||||
| `MISSION_MISSION_ID_REQUIRED` | `4220930` | 422 | Validation error | mission_id is required |
|
||||
| `MISSION_TYPE_INVALID` | `4220931` | 422 | Validation error | mission type is invalid |
|
||||
| `MISSION_SUBTYPE_REQUIRED` | `4220932` | 422 | Validation error | subtype_id is required |
|
||||
| `MISSION_SUBTYPE_INVALID` | `4220933` | 422 | Validation error | subtype_id is invalid |
|
||||
| `MISSION_SUBTYPE_FORBIDDEN` | `4220934` | 422 | Validation error | subtype_id is forbidden |
|
||||
| `MISSION_DUPLICATE_FLIGHT` | `4090935` | 409 | Conflict | mission for this flight already exists |
|
||||
| `MISSION_CREATE_FAILED` | `4000936` | 400 | Bad Request | failed to create mission |
|
||||
| `MISSION_UPDATE_FAILED` | `4000937` | 400 | Bad Request | failed to update mission |
|
||||
| `MISSION_DELETE_FAILED` | `4000938` | 400 | Bad Request | failed to delete mission |
|
||||
| `MISSION_GET_FAILED` | `4000939` | 400 | Bad Request | failed to get mission |
|
||||
| `MISSION_INVALID_MISSION_UUID` | `4220940` | 422 | Validation error | mission uuid is invalid |
|
||||
| `MISSION_LIST_FAILED` | `4000941` | 400 | Bad Request | failed to list mission data |
|
||||
| `MISSION_AFTER_FLIGHT_LOCKED` | `4230942` | 423 | Locked | mission is locked because after flight inspection already exists |
|
||||
|
||||
### Facility (Module `10`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FACILITY_NOT_FOUND` | `4041001` | 404 | Not found | facility not found |
|
||||
| `FACILITY_RELATION_DELETE_ERROR` | `4091002` | 409 | Conflict | cannot delete facility due to active relations |
|
||||
| `FACILITY_INVALID_PAYLOAD` | `4221003` | 422 | Validation error | invalid facility payload |
|
||||
| `FACILITY_INVALID_JSON` | `4001004` | 400 | Bad Request | invalid json payload |
|
||||
| `FACILITY_INVALID_UUID` | `4221005` | 422 | Validation error | facility uuid is invalid |
|
||||
| `FACILITY_ATTRIBUTES_REQUIRED` | `4221006` | 422 | Validation error | at least one facility attribute must be provided |
|
||||
| `FACILITY_CATEGORY_REQUIRED` | `4221007` | 422 | Validation error | facility category is required |
|
||||
| `FACILITY_NAME_REQUIRED` | `4221008` | 422 | Validation error | facility name is required |
|
||||
| `FACILITY_TYPE_REQUIRED` | `4221009` | 422 | Validation error | facility type is required |
|
||||
| `FACILITY_CREATE_FAILED` | `4001010` | 400 | Bad Request | failed to create facility |
|
||||
| `FACILITY_UPDATE_FAILED` | `4001011` | 400 | Bad Request | failed to update facility |
|
||||
| `FACILITY_DELETE_FAILED` | `4001012` | 400 | Bad Request | failed to delete facility |
|
||||
| `FACILITY_LIST_FAILED` | `4001013` | 400 | Bad Request | failed to list facility data |
|
||||
|
||||
### No ICAO Code (Module `11`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `NO_ICAO_CODE_NOT_FOUND` | `4041101` | 404 | Not found | no icao code not found |
|
||||
| `NO_ICAO_CODE_RELATION_DELETE_ERROR` | `4091102` | 409 | Conflict | cannot delete no icao code due to active relations |
|
||||
| `NO_ICAO_CODE_INVALID_PAYLOAD` | `4221103` | 422 | Validation error | invalid no icao code payload |
|
||||
| `NO_ICAO_CODE_INVALID_JSON` | `4001104` | 400 | Bad Request | invalid json payload |
|
||||
| `NO_ICAO_CODE_INVALID_UUID` | `4221105` | 422 | Validation error | no icao code uuid is invalid |
|
||||
| `NO_ICAO_CODE_ID_REQUIRED` | `4221106` | 422 | Validation error | no icao code id is required |
|
||||
| `NO_ICAO_CODE_ID_MISMATCH` | `4221107` | 422 | Validation error | no icao code id mismatch |
|
||||
| `NO_ICAO_CODE_ATTRIBUTES_REQUIRED` | `4221108` | 422 | Validation error | at least one no icao code attribute must be provided |
|
||||
| `NO_ICAO_CODE_NAME_REQUIRED` | `4221109` | 422 | Validation error | no icao code name is required |
|
||||
| `NO_ICAO_CODE_CREATE_FAILED` | `4001110` | 400 | Bad Request | failed to create no icao code |
|
||||
| `NO_ICAO_CODE_UPDATE_FAILED` | `4001111` | 400 | Bad Request | failed to update no icao code |
|
||||
| `NO_ICAO_CODE_DELETE_FAILED` | `4001112` | 400 | Bad Request | failed to delete no icao code |
|
||||
| `NO_ICAO_CODE_LIST_FAILED` | `4001113` | 400 | Bad Request | failed to list no icao code data |
|
||||
|
||||
### Federal State (Module `12`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FEDERAL_STATE_NOT_FOUND` | `4041201` | 404 | Not found | federal state not found |
|
||||
| `FEDERAL_STATE_RELATION_DELETE_ERROR` | `4091202` | 409 | Conflict | cannot delete federal state due to active relations |
|
||||
| `FEDERAL_STATE_INVALID_PAYLOAD` | `4221203` | 422 | Validation error | invalid federal state payload |
|
||||
| `FEDERAL_STATE_INVALID_JSON` | `4001204` | 400 | Bad Request | invalid json payload |
|
||||
| `FEDERAL_STATE_INVALID_ID` | `4221205` | 422 | Validation error | federal state id is invalid |
|
||||
| `FEDERAL_STATE_LAND_ID_INVALID` | `4221206` | 422 | Validation error | federal state land_id is invalid |
|
||||
| `FEDERAL_STATE_ID_REQUIRED` | `4221207` | 422 | Validation error | federal state id is required |
|
||||
| `FEDERAL_STATE_ID_MISMATCH` | `4221208` | 422 | Validation error | federal state id mismatch |
|
||||
| `FEDERAL_STATE_ATTRIBUTES_REQUIRED` | `4221209` | 422 | Validation error | at least one federal state attribute must be provided |
|
||||
| `FEDERAL_STATE_NAME_REQUIRED` | `4221210` | 422 | Validation error | federal state name is required |
|
||||
| `FEDERAL_STATE_CREATE_FAILED` | `4001211` | 400 | Bad Request | failed to create federal state |
|
||||
| `FEDERAL_STATE_UPDATE_FAILED` | `4001212` | 400 | Bad Request | failed to update federal state |
|
||||
| `FEDERAL_STATE_DELETE_FAILED` | `4001213` | 400 | Bad Request | failed to delete federal state |
|
||||
| `FEDERAL_STATE_LIST_FAILED` | `4001214` | 400 | Bad Request | failed to list federal state data |
|
||||
|
||||
### Nationality (Module `13`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `NATIONALITY_NOT_FOUND` | `4041301` | 404 | Not found | nationality not found |
|
||||
| `NATIONALITY_RELATION_DELETE_ERROR` | `4091302` | 409 | Conflict | cannot delete nationality due to active relations |
|
||||
| `NATIONALITY_INVALID_PAYLOAD` | `4221303` | 422 | Validation error | invalid nationality payload |
|
||||
| `NATIONALITY_INVALID_JSON` | `4001304` | 400 | Bad Request | invalid json payload |
|
||||
| `NATIONALITY_INVALID_UUID` | `4221305` | 422 | Validation error | nationality uuid is invalid |
|
||||
| `NATIONALITY_ID_REQUIRED` | `4221306` | 422 | Validation error | nationality id is required |
|
||||
| `NATIONALITY_ID_MISMATCH` | `4221307` | 422 | Validation error | nationality id mismatch |
|
||||
| `NATIONALITY_ATTRIBUTES_REQUIRED` | `4221308` | 422 | Validation error | at least one nationality attribute must be provided |
|
||||
| `NATIONALITY_NAME_REQUIRED` | `4221309` | 422 | Validation error | nationality name is required |
|
||||
| `NATIONALITY_CREATE_FAILED` | `4001310` | 400 | Bad Request | failed to create nationality |
|
||||
| `NATIONALITY_UPDATE_FAILED` | `4001311` | 400 | Bad Request | failed to update nationality |
|
||||
| `NATIONALITY_DELETE_FAILED` | `4001312` | 400 | Bad Request | failed to delete nationality |
|
||||
| `NATIONALITY_LIST_FAILED` | `4001313` | 400 | Bad Request | failed to list nationality data |
|
||||
|
||||
### ICAO (Module `14`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `ICAO_NOT_FOUND` | `4041401` | 404 | Not found | icao not found |
|
||||
| `ICAO_RELATION_DELETE_ERROR` | `4091402` | 409 | Conflict | cannot delete icao due to active relations |
|
||||
| `ICAO_INVALID_PAYLOAD` | `4221403` | 422 | Validation error | invalid icao payload |
|
||||
| `ICAO_INVALID_JSON` | `4001404` | 400 | Bad Request | invalid json payload |
|
||||
| `ICAO_INVALID_UUID` | `4221405` | 422 | Validation error | icao uuid is invalid |
|
||||
| `ICAO_ID_REQUIRED` | `4221406` | 422 | Validation error | icao id is required |
|
||||
| `ICAO_ID_MISMATCH` | `4221407` | 422 | Validation error | icao id mismatch |
|
||||
| `ICAO_ATTRIBUTES_REQUIRED` | `4221408` | 422 | Validation error | at least one icao attribute must be provided |
|
||||
| `ICAO_CODE_REQUIRED` | `4221409` | 422 | Validation error | icao code is required |
|
||||
| `ICAO_LAND_ID_INVALID` | `4221410` | 422 | Validation error | icao land_id is invalid |
|
||||
| `ICAO_FEDERAL_STATE_ID_INVALID` | `4221411` | 422 | Validation error | icao federal_state_id is invalid |
|
||||
| `ICAO_CREATE_FAILED` | `4001412` | 400 | Bad Request | failed to create icao |
|
||||
| `ICAO_UPDATE_FAILED` | `4001413` | 400 | Bad Request | failed to update icao |
|
||||
| `ICAO_DELETE_FAILED` | `4001414` | 400 | Bad Request | failed to delete icao |
|
||||
| `ICAO_LIST_FAILED` | `4001415` | 400 | Bad Request | failed to list icao data |
|
||||
|
||||
### Country (Module `15`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `COUNTRY_NOT_FOUND` | `4041501` | 404 | Not found | country not found |
|
||||
| `COUNTRY_RELATION_DELETE_ERROR` | `4091502` | 409 | Conflict | cannot delete country due to active relations |
|
||||
| `COUNTRY_INVALID_PAYLOAD` | `4221503` | 422 | Validation error | invalid country payload |
|
||||
| `COUNTRY_INVALID_JSON` | `4001504` | 400 | Bad Request | invalid json payload |
|
||||
| `COUNTRY_INVALID_ID` | `4221505` | 422 | Validation error | country id is invalid |
|
||||
| `COUNTRY_ID_REQUIRED` | `4221506` | 422 | Validation error | country id is required |
|
||||
| `COUNTRY_ID_MISMATCH` | `4221507` | 422 | Validation error | country id mismatch |
|
||||
| `COUNTRY_ATTRIBUTES_REQUIRED` | `4221508` | 422 | Validation error | at least one country attribute must be provided |
|
||||
| `COUNTRY_NAME_REQUIRED` | `4221509` | 422 | Validation error | country name is required |
|
||||
| `COUNTRY_ISO_CODE_INVALID` | `4221510` | 422 | Validation error | country iso_code is invalid |
|
||||
| `COUNTRY_ISO_CODE_DUPLICATE` | `4091511` | 409 | Conflict | country iso_code already exists |
|
||||
| `COUNTRY_CREATE_FAILED` | `4001512` | 400 | Bad Request | failed to create country |
|
||||
| `COUNTRY_UPDATE_FAILED` | `4001513` | 400 | Bad Request | failed to update country |
|
||||
| `COUNTRY_DELETE_FAILED` | `4001514` | 400 | Bad Request | failed to delete country |
|
||||
| `COUNTRY_LIST_FAILED` | `4001515` | 400 | Bad Request | failed to list country data |
|
||||
|
||||
### Base (Module `16`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `BASE_NOT_FOUND` | `4041601` | 404 | Not found | base not found |
|
||||
| `BASE_DELETE_CONFLICT` | `4091615` | 409 | Conflict | cannot delete base because it is referenced by another record |
|
||||
| `BASE_INVALID_PAYLOAD` | `4221603` | 422 | Validation error | invalid base payload |
|
||||
| `BASE_INVALID_JSON` | `4001604` | 400 | Bad Request | invalid json payload |
|
||||
| `BASE_INVALID_UUID` | `4221605` | 422 | Validation error | base uuid is invalid |
|
||||
| `BASE_INVALID_CATEGORY` | `4221606` | 422 | Validation error | base category is invalid |
|
||||
| `BASE_ATTRIBUTES_REQUIRED` | `4221607` | 422 | Validation error | at least one base attribute must be provided |
|
||||
| `BASE_NAME_REQUIRED` | `4221608` | 422 | Validation error | base name is required |
|
||||
| `BASE_DEFAULT_SHIFT_START_REQUIRED` | `4221616` | 422 | Validation error | default_shift_start is required |
|
||||
| `BASE_DEFAULT_SHIFT_END_REQUIRED` | `4221617` | 422 | Validation error | default_shift_end is required |
|
||||
| `BASE_CREATE_FAILED` | `4001609` | 400 | Bad Request | failed to create base |
|
||||
| `BASE_UPDATE_FAILED` | `4001610` | 400 | Bad Request | failed to update base |
|
||||
| `BASE_DELETE_FAILED` | `4001611` | 400 | Bad Request | failed to delete base |
|
||||
| `BASE_LIST_FAILED` | `4001612` | 400 | Bad Request | failed to list base data |
|
||||
| `BASE_INVALID_CONTACT_IDS` | `4221613` | 422 | Validation error | base contact ids are invalid |
|
||||
| `BASE_INVALID_TIME` | `4221614` | 422 | Validation error | base time format is invalid |
|
||||
|
||||
Catatan:
|
||||
- `BASE_DELETE_CONFLICT` mengembalikan detail relasi yang masih mengacu ke base, misalnya `takeover`, `dul`, `base operational shift times`, atau `base contact roles`.
|
||||
- Detail pesan bisa berubah sesuai tabel relasi yang ditemukan saat delete.
|
||||
|
||||
### Health Insurance Companies (Module `17`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `HEALTH_INSURANCE_COMPANY_NOT_FOUND` | `4041701` | 404 | Not found | health insurance company not found |
|
||||
| `HEALTH_INSURANCE_COMPANY_RELATION_DELETE_ERROR` | `4091702` | 409 | Conflict | cannot delete health insurance company due to active relations |
|
||||
| `HEALTH_INSURANCE_COMPANY_INVALID_PAYLOAD` | `4221703` | 422 | Validation error | invalid health insurance company payload |
|
||||
| `HEALTH_INSURANCE_COMPANY_INVALID_JSON` | `4001704` | 400 | Bad Request | invalid json payload |
|
||||
| `HEALTH_INSURANCE_COMPANY_INVALID_UUID` | `4221705` | 422 | Validation error | health insurance company uuid is invalid |
|
||||
| `HEALTH_INSURANCE_COMPANY_ID_REQUIRED` | `4221706` | 422 | Validation error | health insurance company id is required |
|
||||
| `HEALTH_INSURANCE_COMPANY_ID_MISMATCH` | `4221707` | 422 | Validation error | health insurance company id mismatch |
|
||||
| `HEALTH_INSURANCE_COMPANY_ATTRIBUTES_REQUIRED` | `4221708` | 422 | Validation error | at least one health insurance company attribute must be provided |
|
||||
| `HEALTH_INSURANCE_COMPANY_NAME_REQUIRED` | `4221709` | 422 | Validation error | health insurance company name is required |
|
||||
| `HEALTH_INSURANCE_COMPANY_LAND_ID_INVALID` | `4221710` | 422 | Validation error | health insurance company land_id is invalid |
|
||||
| `HEALTH_INSURANCE_COMPANY_CREATE_FAILED` | `4001711` | 400 | Bad Request | failed to create health insurance company |
|
||||
| `HEALTH_INSURANCE_COMPANY_UPDATE_FAILED` | `4001712` | 400 | Bad Request | failed to update health insurance company |
|
||||
| `HEALTH_INSURANCE_COMPANY_DELETE_FAILED` | `4001713` | 400 | Bad Request | failed to delete health insurance company |
|
||||
| `HEALTH_INSURANCE_COMPANY_LIST_FAILED` | `4001714` | 400 | Bad Request | failed to list health insurance company data |
|
||||
|
||||
### Vocation (Module `18`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `VOCATION_NOT_FOUND` | `4041801` | 404 | Not found | vocation not found |
|
||||
| `VOCATION_RELATION_DELETE_ERROR` | `4091802` | 409 | Conflict | cannot delete vocation due to active relations |
|
||||
| `VOCATION_INVALID_PAYLOAD` | `4221803` | 422 | Validation error | invalid vocation payload |
|
||||
| `VOCATION_INVALID_JSON` | `4001804` | 400 | Bad Request | invalid json payload |
|
||||
| `VOCATION_INVALID_UUID` | `4221805` | 422 | Validation error | vocation uuid is invalid |
|
||||
| `VOCATION_ID_REQUIRED` | `4221806` | 422 | Validation error | vocation id is required |
|
||||
| `VOCATION_ID_MISMATCH` | `4221807` | 422 | Validation error | vocation id mismatch |
|
||||
| `VOCATION_ATTRIBUTES_REQUIRED` | `4221808` | 422 | Validation error | at least one vocation attribute must be provided |
|
||||
| `VOCATION_NAME_REQUIRED` | `4221809` | 422 | Validation error | vocation name is required |
|
||||
| `VOCATION_CREATE_FAILED` | `4001810` | 400 | Bad Request | failed to create vocation |
|
||||
| `VOCATION_UPDATE_FAILED` | `4001811` | 400 | Bad Request | failed to update vocation |
|
||||
| `VOCATION_DELETE_FAILED` | `4001812` | 400 | Bad Request | failed to delete vocation |
|
||||
| `VOCATION_LIST_FAILED` | `4001813` | 400 | Bad Request | failed to list vocation data |
|
||||
|
||||
### Medicine (Module `19`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `MEDICINE_NOT_FOUND` | `4041901` | 404 | Not found | medicine not found |
|
||||
| `MEDICINE_RELATION_DELETE_ERROR` | `4091902` | 409 | Conflict | cannot delete medicine due to active relations |
|
||||
| `MEDICINE_INVALID_PAYLOAD` | `4221903` | 422 | Validation error | invalid medicine payload |
|
||||
| `MEDICINE_INVALID_JSON` | `4001904` | 400 | Bad Request | invalid json payload |
|
||||
| `MEDICINE_INVALID_UUID` | `4221905` | 422 | Validation error | medicine uuid is invalid |
|
||||
| `MEDICINE_ID_REQUIRED` | `4221906` | 422 | Validation error | medicine id is required |
|
||||
| `MEDICINE_ID_MISMATCH` | `4221907` | 422 | Validation error | medicine id mismatch |
|
||||
| `MEDICINE_ATTRIBUTES_REQUIRED` | `4221908` | 422 | Validation error | at least one medicine attribute must be provided |
|
||||
| `MEDICINE_NAME_REQUIRED` | `4221909` | 422 | Validation error | medicine name is required |
|
||||
| `MEDICINE_GROUP_ID_INVALID` | `4221910` | 422 | Validation error | medicine_group_id is invalid |
|
||||
| `MEDICINE_MOTOR_REACTION_ID_INVALID` | `4221911` | 422 | Validation error | motor_reaction_id is invalid |
|
||||
| `MEDICINE_MOTOR_REACTION_IDS_INVALID` | `4221912` | 422 | Validation error | motor_reaction_ids contains invalid uuid |
|
||||
| `MEDICINE_CREATE_FAILED` | `4001913` | 400 | Bad Request | failed to create medicine |
|
||||
| `MEDICINE_UPDATE_FAILED` | `4001914` | 400 | Bad Request | failed to update medicine |
|
||||
| `MEDICINE_DELETE_FAILED` | `4001915` | 400 | Bad Request | failed to delete medicine |
|
||||
| `MEDICINE_LIST_FAILED` | `4001916` | 400 | Bad Request | failed to list medicine data |
|
||||
|
||||
### Master Settings (Module `20`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `MASTER_SETTINGS_NOT_FOUND` | `4042001` | 404 | Not found | master settings not found |
|
||||
| `MASTER_SETTINGS_INVALID_PAYLOAD` | `4222002` | 422 | Validation error | invalid master settings payload |
|
||||
| `MASTER_SETTINGS_INVALID_JSON` | `4002003` | 400 | Bad Request | invalid json payload |
|
||||
| `MASTER_SETTINGS_INVALID_UUID` | `4222004` | 422 | Validation error | master settings uuid is invalid |
|
||||
| `MASTER_SETTINGS_ID_REQUIRED` | `4222005` | 422 | Validation error | master settings id is required |
|
||||
| `MASTER_SETTINGS_ID_MISMATCH` | `4222006` | 422 | Validation error | master settings id mismatch |
|
||||
| `MASTER_SETTINGS_ATTRIBUTES_REQUIRED` | `4222007` | 422 | Validation error | at least one master settings attribute must be provided |
|
||||
| `MASTER_SETTINGS_COMPANY_NAME_REQUIRED` | `4222008` | 422 | Validation error | company_name is required |
|
||||
| `MASTER_SETTINGS_LOGO_FILE_INVALID` | `4222009` | 422 | Validation error | logo file uuid is invalid |
|
||||
| `MASTER_SETTINGS_COVER_FILE_INVALID` | `4222010` | 422 | Validation error | cover file uuid is invalid |
|
||||
| `MASTER_SETTINGS_CREATE_FAILED` | `4002011` | 400 | Bad Request | failed to create master settings |
|
||||
| `MASTER_SETTINGS_UPDATE_FAILED` | `4002012` | 400 | Bad Request | failed to update master settings |
|
||||
| `MASTER_SETTINGS_DELETE_FAILED` | `4002013` | 400 | Bad Request | failed to delete master settings |
|
||||
| `MASTER_SETTINGS_GET_FAILED` | `4002014` | 400 | Bad Request | failed to get master settings |
|
||||
| `MASTER_SETTINGS_MICROSOFT_ENTRA_UPSERT_FAILED` | `4002015` | 400 | Bad Request | failed to upsert microsoft entra sso settings |
|
||||
|
||||
### Branding (Module `28`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `BRANDING_LOGO_FETCH_FAILED` | `4002801` | 400 | Bad Request | failed to fetch branding logo |
|
||||
| `BRANDING_COVER_FETCH_FAILED` | `4002802` | 400 | Bad Request | failed to fetch branding cover |
|
||||
| `BRANDING_EMAIL_LOGO_FETCH_FAILED` | `4002803` | 400 | Bad Request | failed to fetch branding email logo |
|
||||
| `BRANDING_CONTENT_FETCH_FAILED` | `4002804` | 400 | Bad Request | failed to fetch branding content |
|
||||
| `BRANDING_STORAGE_UNAVAILABLE` | `5032805` | 503 | Internal server error | branding storage service unavailable |
|
||||
| `BRANDING_FILE_MANAGER_UNAVAILABLE` | `5032806` | 503 | Internal server error | branding file manager service unavailable |
|
||||
|
||||
### File Manager (Module `21`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FILE_MANAGER_STORAGE_UNAVAILABLE` | `5032101` | 503 | Storage unavailable | storage service is unavailable |
|
||||
| `FILE_MANAGER_QUEUE_UNAVAILABLE` | `5032102` | 503 | Queue unavailable | queue service is unavailable |
|
||||
| `FILE_MANAGER_UPLOAD_INTENT_UNAVAILABLE` | `5032103` | 503 | Upload intent unavailable | upload intent service is unavailable |
|
||||
| `FILE_MANAGER_ATTACHMENT_SERVICE_UNAVAILABLE` | `5032104` | 503 | Attachment service unavailable | attachment service is unavailable |
|
||||
| `FILE_MANAGER_FOLDER_NOT_FOUND` | `4042105` | 404 | Not found | folder not found |
|
||||
| `FILE_MANAGER_FILE_NOT_FOUND` | `4042106` | 404 | Not found | file not found |
|
||||
| `FILE_MANAGER_ATTACHMENT_NOT_FOUND` | `4042107` | 404 | Not found | attachment not found |
|
||||
| `FILE_MANAGER_UPLOAD_INTENT_NOT_FOUND` | `4042108` | 404 | Not found | upload intent not found |
|
||||
| `FILE_MANAGER_STATUS_CONFLICT` | `4092109` | 409 | Conflict | file status conflict |
|
||||
| `FILE_MANAGER_UPLOAD_INTENT_CONFLICT` | `4092110` | 409 | Conflict | upload intent conflict |
|
||||
| `FILE_MANAGER_VALIDATION_FAILED` | `4222111` | 422 | Validation error | file manager validation failed |
|
||||
| `FILE_MANAGER_ACTION_FAILED` | `4002112` | 400 | Bad Request | file manager action failed |
|
||||
|
||||
### OPC Data (Module `22`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `OPC_NOT_FOUND` | `4042201` | 404 | Not found | opc not found |
|
||||
| `OPC_RELATION_DELETE_ERROR` | `4092202` | 409 | Conflict | cannot delete opc due to active relations |
|
||||
| `OPC_INVALID_PAYLOAD` | `4222203` | 422 | Validation error | invalid opc payload |
|
||||
| `OPC_INVALID_JSON` | `4002204` | 400 | Bad Request | invalid json payload |
|
||||
| `OPC_INVALID_UUID` | `4222205` | 422 | Validation error | opc uuid is invalid |
|
||||
| `OPC_ID_REQUIRED` | `4222206` | 422 | Validation error | opc id is required |
|
||||
| `OPC_ID_MISMATCH` | `4222207` | 422 | Validation error | opc id mismatch |
|
||||
| `OPC_ATTRIBUTES_REQUIRED` | `4222208` | 422 | Validation error | at least one opc attribute must be provided |
|
||||
| `OPC_TITLE_REQUIRED` | `4222209` | 422 | Validation error | opc title is required |
|
||||
| `OPC_CREATE_FAILED` | `4002210` | 400 | Bad Request | failed to create opc |
|
||||
| `OPC_UPDATE_FAILED` | `4002211` | 400 | Bad Request | failed to update opc |
|
||||
| `OPC_DELETE_FAILED` | `4002212` | 400 | Bad Request | failed to delete opc |
|
||||
| `OPC_LIST_FAILED` | `4002213` | 400 | Bad Request | failed to list opc data |
|
||||
|
||||
### Patient Data (Module `23`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `PATIENT_DATA_NOT_FOUND` | `4042301` | 404 | Not found | patient data not found |
|
||||
| `PATIENT_DATA_RELATION_DELETE_ERROR` | `4092302` | 409 | Conflict | cannot delete patient data due to active relations |
|
||||
| `PATIENT_DATA_INVALID_PAYLOAD` | `4222303` | 422 | Validation error | invalid patient data payload |
|
||||
| `PATIENT_DATA_INVALID_JSON` | `4002304` | 400 | Bad Request | invalid json payload |
|
||||
| `PATIENT_DATA_INVALID_UUID` | `4222305` | 422 | Validation error | patient data uuid is invalid |
|
||||
| `PATIENT_DATA_ID_REQUIRED` | `4222306` | 422 | Validation error | patient data id is required |
|
||||
| `PATIENT_DATA_ID_MISMATCH` | `4222307` | 422 | Validation error | patient data id mismatch |
|
||||
| `PATIENT_DATA_ATTRIBUTES_REQUIRED` | `4222308` | 422 | Validation error | at least one patient data attribute must be provided |
|
||||
| `PATIENT_DATA_FAMILY_NAME_REQUIRED` | `4222309` | 422 | Validation error | family_name is required |
|
||||
| `PATIENT_DATA_FIRST_NAME_REQUIRED` | `4222310` | 422 | Validation error | first_name is required |
|
||||
| `PATIENT_DATA_BIRTH_DATE_INVALID` | `4222311` | 422 | Validation error | birth_date is invalid |
|
||||
| `PATIENT_DATA_GENDER_INVALID` | `4222312` | 422 | Validation error | gender is invalid |
|
||||
| `PATIENT_DATA_OPC_ID_INVALID` | `4222313` | 422 | Validation error | opc_id is invalid |
|
||||
| `PATIENT_DATA_INSURANCE_PATIENT_DATA_ID_INVALID` | `4222314` | 422 | Validation error | insurance_patient_data_id is invalid |
|
||||
| `PATIENT_DATA_CREATE_FAILED` | `4002315` | 400 | Bad Request | failed to create patient data |
|
||||
| `PATIENT_DATA_UPDATE_FAILED` | `4002316` | 400 | Bad Request | failed to update patient data |
|
||||
| `PATIENT_DATA_DELETE_FAILED` | `4002317` | 400 | Bad Request | failed to delete patient data |
|
||||
| `PATIENT_DATA_LIST_FAILED` | `4002318` | 400 | Bad Request | failed to list patient data |
|
||||
|
||||
### Insurance Patient Data (Module `24`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `INSURANCE_PATIENT_DATA_NOT_FOUND` | `4042401` | 404 | Not found | insurance patient data not found |
|
||||
| `INSURANCE_PATIENT_DATA_RELATION_DELETE_ERROR` | `4092402` | 409 | Conflict | cannot delete insurance patient data due to active relations |
|
||||
| `INSURANCE_PATIENT_DATA_INVALID_PAYLOAD` | `4222403` | 422 | Validation error | invalid insurance patient data payload |
|
||||
| `INSURANCE_PATIENT_DATA_INVALID_JSON` | `4002404` | 400 | Bad Request | invalid json payload |
|
||||
| `INSURANCE_PATIENT_DATA_INVALID_UUID` | `4222405` | 422 | Validation error | insurance patient data uuid is invalid |
|
||||
| `INSURANCE_PATIENT_DATA_ID_REQUIRED` | `4222406` | 422 | Validation error | insurance patient data id is required |
|
||||
| `INSURANCE_PATIENT_DATA_ID_MISMATCH` | `4222407` | 422 | Validation error | insurance patient data id mismatch |
|
||||
| `INSURANCE_PATIENT_DATA_ATTRIBUTES_REQUIRED` | `4222408` | 422 | Validation error | at least one insurance patient data attribute must be provided |
|
||||
| `INSURANCE_PATIENT_DATA_HEALTH_INSURANCE_ID_INVALID` | `4222409` | 422 | Validation error | health_insurance_companies_id is invalid |
|
||||
| `INSURANCE_PATIENT_DATA_FEDERAL_STATE_ID_INVALID` | `4222410` | 422 | Validation error | federal_state_id is invalid |
|
||||
| `INSURANCE_PATIENT_DATA_CREATE_FAILED` | `4002411` | 400 | Bad Request | failed to create insurance patient data |
|
||||
| `INSURANCE_PATIENT_DATA_UPDATE_FAILED` | `4002412` | 400 | Bad Request | failed to update insurance patient data |
|
||||
| `INSURANCE_PATIENT_DATA_DELETE_FAILED` | `4002413` | 400 | Bad Request | failed to delete insurance patient data |
|
||||
| `INSURANCE_PATIENT_DATA_LIST_FAILED` | `4002414` | 400 | Bad Request | failed to list insurance patient data |
|
||||
|
||||
### Role (Module `25`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `ROLE_NOT_FOUND` | `4042501` | 404 | Not found | role not found |
|
||||
| `ROLE_PERMISSION_NOT_FOUND` | `4042502` | 404 | Not found | permission not found |
|
||||
| `ROLE_RELATION_DELETE_ERROR` | `4092503` | 409 | Conflict | cannot delete role due to active relations |
|
||||
| `ROLE_INVALID_PAYLOAD` | `4222504` | 422 | Validation error | invalid role payload |
|
||||
| `ROLE_INVALID_JSON` | `4002505` | 400 | Bad Request | invalid json payload |
|
||||
| `ROLE_INVALID_UUID` | `4222506` | 422 | Validation error | role uuid is invalid |
|
||||
| `ROLE_INVALID_PERMISSION_UUID` | `4222507` | 422 | Validation error | permission uuid is invalid |
|
||||
| `ROLE_ID_REQUIRED` | `4222508` | 422 | Validation error | role id is required |
|
||||
| `ROLE_ID_MISMATCH` | `4222509` | 422 | Validation error | role id mismatch |
|
||||
| `ROLE_ATTRIBUTES_REQUIRED` | `4222510` | 422 | Validation error | at least one role attribute must be provided |
|
||||
| `ROLE_NAME_REQUIRED` | `4222511` | 422 | Validation error | role name is required |
|
||||
| `ROLE_PERMISSION_ID_REQUIRED` | `4222512` | 422 | Validation error | permission_id is required |
|
||||
| `ROLE_PERMISSION_IDS_EMPTY` | `4222513` | 422 | Validation error | permission_ids must not be empty |
|
||||
| `ROLE_CREATE_FAILED` | `4002514` | 400 | Bad Request | failed to create role |
|
||||
| `ROLE_UPDATE_FAILED` | `4002515` | 400 | Bad Request | failed to update role |
|
||||
| `ROLE_DELETE_FAILED` | `4002516` | 400 | Bad Request | failed to delete role |
|
||||
| `ROLE_ASSIGN_FAILED` | `4002517` | 400 | Bad Request | failed to assign permission |
|
||||
| `ROLE_REMOVE_FAILED` | `4002518` | 400 | Bad Request | failed to remove permission |
|
||||
| `ROLE_LIST_FAILED` | `4002519` | 400 | Bad Request | failed to list role data |
|
||||
| `ROLE_PERMISSION_LIST_FAILED` | `4002520` | 400 | Bad Request | failed to list permission data |
|
||||
| `ROLE_PERMISSION_UPDATE_FAILED` | `4002521` | 400 | Bad Request | failed to update permission |
|
||||
| `ROLE_PERMISSION_REQUIRES_PIN_REQUIRED` | `4222522` | 422 | Validation error | requires_pin is required |
|
||||
|
||||
### Flight Data (Module `26`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `FLIGHT_DATA_NOT_FOUND` | `4042601` | 404 | Not found | flight data not found |
|
||||
| `FLIGHT_DATA_INVALID_PAYLOAD` | `4222602` | 422 | Validation error | invalid flight data payload |
|
||||
| `FLIGHT_DATA_INVALID_JSON` | `4002603` | 400 | Bad Request | invalid json payload |
|
||||
| `FLIGHT_DATA_INVALID_PATH_UUID` | `4222604` | 422 | Validation error | path uuid is invalid |
|
||||
| `FLIGHT_DATA_MISSION_ID_REQUIRED` | `4222605` | 422 | Validation error | mission_id is required |
|
||||
| `FLIGHT_DATA_PILOT_ID_REQUIRED` | `4222606` | 422 | Validation error | pilot_id is required |
|
||||
| `FLIGHT_DATA_INVALID_UUID` | `4222607` | 422 | Validation error | uuid is invalid |
|
||||
| `FLIGHT_DATA_INVALID_RFC3339` | `4222608` | 422 | Validation error | datetime must be RFC3339 |
|
||||
| `FLIGHT_DATA_ORIGIN_LOCATION_REFERENCE` | `4222609` | 422 | Validation error | origin location reference is invalid |
|
||||
| `FLIGHT_DATA_DESTINATION_LOCATION_REFERENCE` | `4222610` | 422 | Validation error | destination location reference is invalid |
|
||||
| `FLIGHT_DATA_REQUIRED` | `4002611` | 400 | Bad Request | required flight data is missing |
|
||||
| `FLIGHT_DATA_CREATE_FAILED` | `4002612` | 400 | Bad Request | failed to create flight data |
|
||||
| `FLIGHT_DATA_GET_FAILED` | `4002613` | 400 | Bad Request | failed to get flight data |
|
||||
| `FLIGHT_DATA_AFTER_FLIGHT_INCOMPLETE` | `4222616` | 422 | Validation error | cannot submit after-flight: flight data is incomplete |
|
||||
| `FLIGHT_DATA_CREATE_BLOCKED` | `4092618` | 409 | Conflict | flight data cannot be created because fm report already exists for this flight |
|
||||
| `FLIGHT_DATA_AFTER_FLIGHT_LOCKED` | `4232618` | 423 | Locked | flight data is locked because after flight inspection already exists |
|
||||
| `FLIGHT_DATA_LOCKED` | `4232619` | 423 | Locked | flight data is locked because fm report is completed |
|
||||
|
||||
### Air Rescuer Checklist (Module `27`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `AIR_RESCUER_CHECKLIST_NOT_FOUND` | `4042701` | 404 | Not found | air rescuer checklist not found |
|
||||
| `AIR_RESCUER_CHECKLIST_ITEM_NOT_FOUND` | `4042702` | 404 | Not found | air rescuer checklist item not found |
|
||||
| `AIR_RESCUER_CHECKLIST_INVALID_PAYLOAD` | `4222703` | 422 | Validation error | invalid air rescuer checklist payload |
|
||||
| `AIR_RESCUER_CHECKLIST_INVALID_JSON` | `4002704` | 400 | Bad Request | invalid json payload |
|
||||
| `AIR_RESCUER_CHECKLIST_INVALID_UUID` | `4222705` | 422 | Validation error | air rescuer checklist uuid is invalid |
|
||||
| `AIR_RESCUER_CHECKLIST_ID_MISMATCH` | `4222706` | 422 | Validation error | id does not match path uuid |
|
||||
| `AIR_RESCUER_CHECKLIST_SCOPE_INVALID` | `4222707` | 422 | Validation error | scope_code is invalid |
|
||||
| `AIR_RESCUER_CHECKLIST_SCOPE_REQUIRED` | `4222708` | 422 | Validation error | scope_code is required |
|
||||
| `AIR_RESCUER_CHECKLIST_TITLE_REQUIRED` | `4222709` | 422 | Validation error | title is required |
|
||||
| `AIR_RESCUER_CHECKLIST_NAME_REQUIRED` | `4222710` | 422 | Validation error | name is required |
|
||||
| `AIR_RESCUER_CHECKLIST_BASE_NOT_FOUND` | `4042711` | 404 | Not found | base not found |
|
||||
| `AIR_RESCUER_CHECKLIST_CREATE_FAILED` | `4002712` | 400 | Bad Request | failed to create air rescuer checklist |
|
||||
| `AIR_RESCUER_CHECKLIST_UPDATE_FAILED` | `4002713` | 400 | Bad Request | failed to update air rescuer checklist |
|
||||
| `AIR_RESCUER_CHECKLIST_DELETE_FAILED` | `4002714` | 400 | Bad Request | failed to delete air rescuer checklist |
|
||||
| `AIR_RESCUER_CHECKLIST_LIST_FAILED` | `4002715` | 400 | Bad Request | failed to list air rescuer checklist |
|
||||
| `AIR_RESCUER_CHECKLIST_REORDER_FAILED` | `4002716` | 400 | Bad Request | failed to reorder air rescuer checklist items |
|
||||
| `AIR_RESCUER_CHECKLIST_VALIDATION_FAILED` | `4222717` | 422 | Validation error | air rescuer checklist validation failed |
|
||||
|
||||
### After Flight Inspection (Module `37`)
|
||||
|
||||
| Code | Error Code | HTTP | Title | Message |
|
||||
|---|---:|---:|---|---|
|
||||
| `AFTER_FLIGHT_INVALID_ID` | `4223701` | 422 | Validation error | flight_inspection_id is invalid |
|
||||
| `AFTER_FLIGHT_FLIGHT_INSPECTION_NOT_FOUND` | `4043702` | 404 | Not found | flight inspection not found |
|
||||
| `AFTER_FLIGHT_INSPECTION_NOT_FOUND` | `4043703` | 404 | Not found | after flight inspection not found |
|
||||
| `AFTER_FLIGHT_CAPABILITIES_UNAVAILABLE` | `4223704` | 422 | Validation error | helicopter capabilities could not be resolved |
|
||||
| `AFTER_FLIGHT_NR1_REQUIRED` | `4223705` | 422 | Validation error | oil_engine_nr1_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_NR1_NOT_AVAILABLE` | `4223706` | 422 | Validation error | NR1 capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_NR2_REQUIRED` | `4223707` | 422 | Validation error | oil_engine_nr2_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_NR2_NOT_AVAILABLE` | `4223708` | 422 | Validation error | NR2 capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_LH_REQUIRED` | `4223709` | 422 | Validation error | hydraulic_lh_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_LH_NOT_AVAILABLE` | `4223710` | 422 | Validation error | LH capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_RH_REQUIRED` | `4223711` | 422 | Validation error | hydraulic_rh_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_RH_NOT_AVAILABLE` | `4223712` | 422 | Validation error | RH capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_MGB_REQUIRED` | `4223713` | 422 | Validation error | oil_transmission_mgb_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_MGB_NOT_AVAILABLE` | `4223714` | 422 | Validation error | MGB capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_IGB_REQUIRED` | `4223715` | 422 | Validation error | oil_transmission_igb_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_IGB_NOT_AVAILABLE` | `4223716` | 422 | Validation error | IGB capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_TGB_REQUIRED` | `4223717` | 422 | Validation error | oil_transmission_tgb_checked is required for this helicopter |
|
||||
| `AFTER_FLIGHT_TGB_NOT_AVAILABLE` | `4223718` | 422 | Validation error | TGB capability not available on this helicopter |
|
||||
| `AFTER_FLIGHT_INVALID_JSON` | `4003719` | 400 | Bad Request | invalid json payload |
|
||||
|
||||
## Notes
|
||||
|
||||
- `title` dan `detail` dapat berubah mengikuti copywriting; jangan dijadikan kontrak parsing.
|
||||
- `code` dan `error_code` harus dianggap stabil sebagai kontrak integrasi BE↔FE.
|
||||
- Uniqueness `code` dan `error_code` divalidasi lewat test registry di package [apperrorsx](../internal/shared/pkg/apperrorsx) (`registry_test.go`).
|
||||
- Sumber definisi tunggal: [codes_defs.go](../internal/shared/pkg/apperrorsx/codes_defs.go) (konstanta `Code*`, `Case*`, dan variable `Err*` + `Registry`).
|
||||
- Jika menambah error baru: daftarkan di `codes_defs.go` (konstanta + variable `Err*`) lalu masukkan ke `Registry`, kemudian update tabel di catalog ini.
|
||||
124
docs/file-lifecycle-s3-cleanup.md
Normal file
124
docs/file-lifecycle-s3-cleanup.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# File Lifecycle and S3 Cleanup
|
||||
|
||||
## Lifecycle States (DB source of truth)
|
||||
|
||||
`file_files.lifecycle_status`:
|
||||
|
||||
- `temp`: uploaded but not attached
|
||||
- `active`: referenced by one or more business entities
|
||||
- `orphan_pending`: currently unreferenced, waiting grace period
|
||||
- `deleted`: backend cleanup has deleted object(s) and marked terminal state
|
||||
|
||||
Supporting timestamps:
|
||||
|
||||
- `created_at`: existing file creation time
|
||||
- `attached_at`: set when file becomes active
|
||||
- `orphaned_at`: set when file becomes unreferenced
|
||||
- `lifecycle_deleted_at`: set when cleanup marks lifecycle `deleted`
|
||||
|
||||
Notes:
|
||||
|
||||
- Existing `status` column (`uploaded/processing/validated/ready/trashed`) remains for processing/trash flow.
|
||||
- `deleted_at` is still used by trash semantics. Cleanup lifecycle uses `lifecycle_deleted_at`.
|
||||
|
||||
## S3 Tags
|
||||
|
||||
Single tag key: `gc`
|
||||
|
||||
- `gc=temp`: temporary uploads
|
||||
- `gc=keep`: active or protected files
|
||||
- `gc=delete`: optional marker immediately before backend delete
|
||||
|
||||
S3 tags are markers only. DB lifecycle state and DB reference checks are authoritative.
|
||||
|
||||
## Cleanup Job
|
||||
|
||||
Scheduler: API process loop (`startFileManagerLifecycleCleanupLoop`).
|
||||
|
||||
Behavior:
|
||||
|
||||
1. Query DB for `orphan_pending` files where `orphaned_at <= now - ORPHAN_GRACE_PERIOD_DAYS`.
|
||||
2. Process in batches (`FILE_CLEANUP_BATCH_SIZE`).
|
||||
3. Re-check DB references per file before deletion.
|
||||
4. If referenced again: mark `active`, set/keep `gc=keep`.
|
||||
5. If still unreferenced:
|
||||
- optional `gc=delete` tag
|
||||
- delete original and thumbnail S3 objects
|
||||
- mark lifecycle `deleted` and set `lifecycle_deleted_at`
|
||||
6. Per-file error handling; one failure does not stop the batch.
|
||||
|
||||
The job is idempotent and retry-safe.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `ENABLE_S3_TAGGING` (default `true`)
|
||||
- `ENABLE_FILE_CLEANUP_JOB` (default `false`)
|
||||
- `TEMP_FILE_TTL_DAYS` (default `7`)
|
||||
- `ORPHAN_GRACE_PERIOD_DAYS` (default `30`)
|
||||
- `FILE_CLEANUP_BATCH_SIZE` (default `100`)
|
||||
- `FILE_CLEANUP_DRY_RUN` (default `true`)
|
||||
|
||||
## Backfill (Dry-Run First)
|
||||
|
||||
Command:
|
||||
|
||||
```bash
|
||||
go run ./cmd/file-lifecycle-backfill
|
||||
```
|
||||
|
||||
Optional:
|
||||
|
||||
- `BACKFILL_DRY_RUN=true|false` (default `true`)
|
||||
|
||||
Output report includes:
|
||||
|
||||
- active files
|
||||
- probable orphan files
|
||||
- missing S3 key
|
||||
- invalid references
|
||||
- skipped records
|
||||
|
||||
## Required IAM Permissions
|
||||
|
||||
Minimum required:
|
||||
|
||||
- `s3:PutObjectTagging`
|
||||
- `s3:GetObjectTagging`
|
||||
- `s3:DeleteObject`
|
||||
- `s3:GetObject`
|
||||
- `s3:HeadObject`
|
||||
|
||||
Only if infra automation updates lifecycle rules:
|
||||
|
||||
- `s3:PutLifecycleConfiguration`
|
||||
|
||||
## S3 Lifecycle Rules (Manual if no IaC)
|
||||
|
||||
Because this repository does not define Terraform/CDK/CloudFormation for S3 bucket lifecycle, configure lifecycle rules in your infra/deployment stack:
|
||||
|
||||
1. Rule `gc=temp`:
|
||||
- Filter: object tag `gc=temp`
|
||||
- Expire after `TEMP_FILE_TTL_DAYS` (recommended default 7 days)
|
||||
|
||||
2. Optional rule `gc=delete`:
|
||||
- Filter: object tag `gc=delete`
|
||||
- Expire after 1 day
|
||||
|
||||
Do not use lifecycle expiration as the orphan grace mechanism. Grace period is enforced by DB `orphaned_at`.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. Deploy schema + code with defaults (`cleanup job off`, `dry-run on`).
|
||||
2. Enable tagging for new uploads (`ENABLE_S3_TAGGING=true`).
|
||||
3. Validate attach/detach transitions in logs.
|
||||
4. Run backfill dry-run and review report.
|
||||
5. Enable cleanup job in dry-run mode.
|
||||
6. Switch cleanup dry-run off after validation.
|
||||
7. Apply S3 lifecycle rules for `gc=temp` (and optional `gc=delete`).
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
1. Set `ENABLE_FILE_CLEANUP_JOB=false`.
|
||||
2. Set `FILE_CLEANUP_DRY_RUN=true`.
|
||||
3. Optionally set `ENABLE_S3_TAGGING=false`.
|
||||
4. Pause/remove lifecycle rule `gc=delete` if enabled.
|
||||
143
docs/file-manager-aircraft-upload-create.md
Normal file
143
docs/file-manager-aircraft-upload-create.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# File Manager Aircraft Image Flow
|
||||
|
||||
This document explains the upload + create flow specifically for aircraft images.
|
||||
|
||||
## Purpose
|
||||
|
||||
This flow is used to upload aircraft images (drag-and-drop) and persist file metadata into a forced folder hierarchy based on the aircraft designation.
|
||||
|
||||
Main differences compared to the regular file flow:
|
||||
- dedicated upload endpoint: `POST /api/v1/file-manager/files/upload-aircraft-image`
|
||||
- dedicated create endpoint: `POST /api/v1/file-manager/files/create-aircraft-image`
|
||||
- `content_type` must be `image/*`
|
||||
- target folder is forced to: `__system_root_files__ > aircraft > <aircraft designation>`
|
||||
- `aircraft_uuid` is required in the create request per item
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. User is logged in and has `file_manager.create` permission.
|
||||
2. Frontend has per-file data: `name`, `content_type`, `size_bytes`, and binary file.
|
||||
|
||||
## Step 1 - Request Upload Intent (Aircraft)
|
||||
|
||||
Endpoint:
|
||||
- `POST /api/v1/file-manager/files/upload-aircraft-image`
|
||||
|
||||
Request body (bulk, minimum 1 item):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"attributes": {
|
||||
"name": "aircraft-front.jpg",
|
||||
"content_type": "image/jpeg",
|
||||
"size_bytes": 42170
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Important validation rules in this endpoint:
|
||||
- `data` must be an array
|
||||
- `type` must be `file_manager_file_upload_intent`
|
||||
- `name` is required
|
||||
- `content_type` is required and must start with `image/`
|
||||
- `size_bytes >= 0`
|
||||
|
||||
Per-item response (partial success is possible), FE receives:
|
||||
- `upload_intent_uuid`
|
||||
- `upload_url`
|
||||
- `method` (`PUT`)
|
||||
- `required_headers`
|
||||
- object info (`bucket`, `key`)
|
||||
- `expires_at`, `expires_in_seconds`
|
||||
|
||||
## Step 2 - Upload Binary Directly to Object Storage
|
||||
|
||||
For each item with `success: true` from step 1:
|
||||
1. take `upload_url`
|
||||
2. perform `PUT` binary file to that URL
|
||||
3. send headers according to `required_headers`
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
await fetch(upload_url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": file.type
|
||||
},
|
||||
body: file
|
||||
});
|
||||
```
|
||||
|
||||
Notes:
|
||||
- 1 file = 1 `PUT` request
|
||||
- parallel upload is allowed with a concurrency limit
|
||||
- files that fail `PUT` must not be sent to the create step
|
||||
|
||||
## Step 3 - Create File Record (Aircraft)
|
||||
|
||||
Endpoint:
|
||||
- `POST /api/v1/file-manager/files/create-aircraft-image`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_create",
|
||||
"attributes": {
|
||||
"upload_intent_uuid": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"aircraft_uuid": "019d8215-c799-7209-9237-2e2f5d0f8709",
|
||||
"name": "aircraft-front.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Important validation rules:
|
||||
- `data` must be an array
|
||||
- `type` must be `file_manager_file_create` or `file_manager_file`
|
||||
- `upload_intent_uuid` is required and must be a valid UUID
|
||||
- `aircraft_uuid` is required and must be a valid UUID
|
||||
- `name` is required
|
||||
|
||||
Aircraft-specific behavior:
|
||||
- backend loads aircraft by `aircraft_uuid` and reads its `designation`
|
||||
- backend resolves/creates folder hierarchy:
|
||||
- `__system_root_files__` (system root)
|
||||
- `aircraft` under system root
|
||||
- `<designation>` under `aircraft` (example: `OE-XHZ`)
|
||||
- final file is created inside `<designation>` folder
|
||||
- `folder_id` from payload is ignored in this endpoint because folder is derived from aircraft designation
|
||||
|
||||
## Step 4 - Backend Process During Create
|
||||
|
||||
For each valid item:
|
||||
1. backend validates and loads aircraft from `aircraft_uuid`
|
||||
2. backend resolves/creates `__system_root_files__ > aircraft > <designation>`
|
||||
3. backend validates uploaded object from `upload_intent_uuid`
|
||||
4. backend creates file node in `<designation>` folder
|
||||
5. file status enters normal file manager lifecycle (`uploaded -> processing -> validated -> ready` or failed)
|
||||
|
||||
## Status Codes
|
||||
|
||||
- `201`: all create items succeed
|
||||
- `207`: partial success (mixed success and failure)
|
||||
- `404`: aircraft not found (per-item in bulk response)
|
||||
- `422`: payload validation error
|
||||
- `400`: invalid JSON payload
|
||||
- `503`: aircraft service unavailable
|
||||
|
||||
## FE Integration Summary
|
||||
|
||||
1. call `upload-aircraft-image`
|
||||
2. upload binary to storage via presigned URL
|
||||
3. call `create-aircraft-image` only for items with successful `PUT`
|
||||
4. store resulting file `id` for reference in aircraft module
|
||||
73
docs/file-manager-attachment.md
Normal file
73
docs/file-manager-attachment.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# File Manager
|
||||
|
||||
# Attachment (Step by Step)
|
||||
|
||||
## A. Tambah Attachment
|
||||
|
||||
### Endpoint
|
||||
- `POST /api/v1/file-manager/attachments/create`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_attachment_create",
|
||||
"attributes": {
|
||||
"file_id": "file-uuid",
|
||||
"ref_type": "helicopter",
|
||||
"ref_id": "business-entity-uuid",
|
||||
"category": "document",
|
||||
"sort_order": 0,
|
||||
"is_primary": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keterangan:
|
||||
- `file_id`, `ref_type`, `ref_id` wajib
|
||||
- `category` opsional
|
||||
- `sort_order` opsional (default 0)
|
||||
- `is_primary` opsional (default false)
|
||||
|
||||
## B. Lihat Attachment by Reference
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/attachments/get-all?ref_type=<type>&ref_id=<id>`
|
||||
|
||||
### Contoh
|
||||
- `GET /api/v1/file-manager/attachments/get-all?ref_type=helicopter&ref_id=business-entity-uuid`
|
||||
|
||||
Catatan urutan default:
|
||||
- jika query `sort` tidak dikirim, urutan default adalah `sort_order ASC, created_at DESC`
|
||||
- artinya untuk `sort_order` yang sama, attachment terbaru tampil lebih dulu
|
||||
|
||||
## C. Detail Attachment
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/attachments/get/:uuid`
|
||||
|
||||
## D. Hapus Attachment (Detach)
|
||||
|
||||
### Endpoint
|
||||
- `DELETE /api/v1/file-manager/attachments/delete/:uuid`
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_attachment_delete",
|
||||
"attributes": {
|
||||
"detached": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ringkasan
|
||||
1. create relation file -> entity bisnis
|
||||
2. list by `ref_type + ref_id`
|
||||
3. get detail relation
|
||||
4. delete relation (detach)
|
||||
62
docs/file-manager-base-upload-create.md
Normal file
62
docs/file-manager-base-upload-create.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# File Manager Base Image Flow
|
||||
|
||||
This document explains upload + create flow specifically for base images.
|
||||
|
||||
## Purpose
|
||||
|
||||
This flow uploads base images and persists file metadata into a forced folder hierarchy:
|
||||
|
||||
- `__system_root_files__ > bases > <base-type> > <base-name>`
|
||||
|
||||
Main differences compared to regular file flow:
|
||||
|
||||
- dedicated upload endpoint: `POST /api/v1/file-manager/files/upload-base-image`
|
||||
- dedicated create endpoint: `POST /api/v1/file-manager/files/create-base-image`
|
||||
- `content_type` must be `image/*`
|
||||
- `base_uuid` is required in the create request per item
|
||||
- create step performs hard replace of existing files in target folder before creating new file
|
||||
|
||||
## Step 1 - Request Upload Intent (Base)
|
||||
|
||||
Endpoint:
|
||||
|
||||
- `POST /api/v1/file-manager/files/upload-base-image`
|
||||
|
||||
Validation highlights:
|
||||
|
||||
- `type` must be `file_manager_file_upload_intent`
|
||||
- `name` required
|
||||
- `content_type` required and must start with `image/`
|
||||
- `size_bytes >= 0`
|
||||
|
||||
## Step 2 - Upload Binary to Object Storage
|
||||
|
||||
For each successful intent, frontend uploads binary with `PUT` to `upload_url`.
|
||||
|
||||
## Step 3 - Create File Record (Base)
|
||||
|
||||
Endpoint:
|
||||
|
||||
- `POST /api/v1/file-manager/files/create-base-image`
|
||||
|
||||
Validation highlights:
|
||||
|
||||
- `type` must be `file_manager_file_create` or `file_manager_file`
|
||||
- `upload_intent_uuid` required and valid UUID
|
||||
- `base_uuid` required and valid UUID
|
||||
- `name` required
|
||||
|
||||
Backend behavior:
|
||||
|
||||
1. load base from `base_uuid`
|
||||
2. resolve `base-type` from base category (e.g. `hems` or `regular`)
|
||||
3. resolve/create folder path `__system_root_files__ > bases > <base-type> > <base-name>`
|
||||
4. hard replace: delete + purge all existing files in that target folder
|
||||
5. create file node from upload intent
|
||||
|
||||
Hard replace cleanup target:
|
||||
|
||||
- file row in DB
|
||||
- original object in S3
|
||||
- thumbnail object in S3 (if exists)
|
||||
- related attachment metadata
|
||||
829
docs/file-manager-collabora-wopi-flow.md
Normal file
829
docs/file-manager-collabora-wopi-flow.md
Normal file
@@ -0,0 +1,829 @@
|
||||
# File Manager Collabora / WOPI Flow
|
||||
|
||||
Dokumen ini menjelaskan alur frontend untuk membuka editor Collabora lewat backend WOPI.
|
||||
|
||||
Prinsip utama:
|
||||
|
||||
1. Frontend hanya memanggil App API.
|
||||
2. Frontend tidak pernah memanggil endpoint `/wopi/...` secara langsung.
|
||||
3. Backend yang mengurus token, proof, S3, lock, autosave, dan finalisasi.
|
||||
|
||||
Tambahan penting untuk flow helicopter template:
|
||||
|
||||
1. hasil `create-from-template` akan dibuat sebagai file manager file biasa, bukan template
|
||||
2. backend juga akan memastikan file itu masuk ke `helicopter_files` section `fpwb`
|
||||
3. kalau frontend membuka template yang sama untuk `takeover_id` yang sama, backend akan reuse file existing bila sudah ada
|
||||
4. saat child `fpwb` baru berhasil dibuat, backend akan kirim SSE agar frontend bisa langsung append item baru ke list
|
||||
5. kalau file hasil template punya `takeover_id`, endpoint `GET /api/v1/helicopter-files/get/{helicopter_id}` dan `GET /api/v1/helicopter-files/list` akan menyembunyikan item itu dari user yang tidak punya akses takeover tersebut
|
||||
|
||||
## Endpoint Yang Dipakai Frontend
|
||||
|
||||
### 1) Buat file baru dari template
|
||||
|
||||
`POST /api/v1/file-manager/files/create-from-template`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_file_create_from_template",
|
||||
"attributes": {
|
||||
"template_uuid": "019f0386-0a00-7a4a-a2d8-30afc0e0c3ad",
|
||||
"helicopter_id": "019e8713-aa19-7fb7-a510-a0332dc69697",
|
||||
"takeover_id": "019f0386-0a00-7a4a-a2d8-30afc0e0c3af",
|
||||
"name": "collabora-draft.docx"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"file_id": "019f0a12-1111-7abc-8def-222222222222",
|
||||
"collabora_action": "https://collabora.example.com/.../WOPISrc=...",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"access_token_ttl": 1800000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2) Buka editor untuk file existing
|
||||
|
||||
`GET /api/v1/file-manager/files/{fileId}/edit-session`
|
||||
|
||||
Response shape sama:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"file_id": "019f0a12-1111-7abc-8def-222222222222",
|
||||
"collabora_action": "https://collabora.example.com/.../WOPISrc=...",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"access_token_ttl": 1800000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3) Ambil daftar helicopter files
|
||||
|
||||
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
|
||||
Endpoint ini yang dipakai frontend untuk melihat child file yang muncul di helicopter file collection.
|
||||
|
||||
Untuk hasil `create-from-template`, item baru akan muncul di:
|
||||
|
||||
1. `data.attributes.files.prepare`
|
||||
2. section backend = `fpwb`
|
||||
3. secara UI biasanya ditampilkan sebagai area Flight Preparation and WB
|
||||
|
||||
### 4) Dengarkan event child baru
|
||||
|
||||
`GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}`
|
||||
|
||||
Stream ini dipakai supaya frontend bisa langsung append child baru tanpa reload penuh.
|
||||
|
||||
Event name:
|
||||
|
||||
`helicopter.file.created`
|
||||
|
||||
### SSE dapet dari mana?
|
||||
|
||||
SSE ini bukan datang dari Collabora, bukan dari browser local state, dan bukan dari endpoint `/wopi/...`.
|
||||
|
||||
Sumbernya adalah backend App API:
|
||||
|
||||
1. frontend subscribe ke `GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}`
|
||||
2. route ini masuk ke handler SSE backend
|
||||
3. backend hanya akan mengirim event ke user yang sedang subscribe
|
||||
4. event difilter lagi dengan `helicopter_id` query param
|
||||
|
||||
Kapan event dipublish:
|
||||
|
||||
1. frontend memanggil `POST /api/v1/file-manager/files/create-from-template`
|
||||
2. backend membuat atau reuse file manager file
|
||||
3. backend memastikan ada row `helicopter_files` di section `fpwb`
|
||||
4. kalau row `helicopter_files` itu baru dibuat saat request ini, backend publish event `helicopter.file.created`
|
||||
5. stream `/api/v1/helicopter-files/events` mengirim event itu ke frontend yang subscribe
|
||||
|
||||
Artinya:
|
||||
|
||||
1. kalau child `fpwb` sudah ada dari sebelumnya, backend tidak akan publish event create lagi
|
||||
2. kalau frontend telat subscribe, fallback paling aman tetap re-fetch `GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
3. SSE ini hanya untuk sinkronisasi UI realtime, bukan source of truth utama
|
||||
|
||||
## Flow Create From Template
|
||||
|
||||
### Step 1 - Frontend minta editor session
|
||||
|
||||
Frontend mengirim `template_uuid`, `helicopter_id`, `name`, dan boleh menambahkan `takeover_id`.
|
||||
`template_uuid` harus mengarah ke `helicopter_file.helicopter_file_id` dari helicopter yang sama dengan `helicopter_id`, bukan ke file takeover biasa.
|
||||
|
||||
Backend akan menjalankan alur seperti ini:
|
||||
|
||||
```text
|
||||
create-from-template
|
||||
-> cek user session
|
||||
-> cek template memang milik helicopter itu
|
||||
-> jika ada takeover_id:
|
||||
-> cek apakah kombinasi template + takeover sudah punya file existing
|
||||
-> jika sudah ada:
|
||||
-> reuse file existing
|
||||
-> pastikan metadata template + takeover tersimpan
|
||||
-> pastikan ada child helicopter_files di section fpwb
|
||||
-> jika child baru dibuat, publish SSE helicopter.file.created
|
||||
-> mint WOPI token
|
||||
-> balikin Collabora action URL
|
||||
-> jika belum ada:
|
||||
-> buat draft file baru dari template
|
||||
-> copy object template ke key final file baru
|
||||
-> simpan metadata template + takeover
|
||||
-> pastikan ada child helicopter_files di section fpwb
|
||||
-> jika child baru dibuat, publish SSE helicopter.file.created
|
||||
-> mint WOPI token
|
||||
-> balikin Collabora action URL
|
||||
-> jika takeover_id tidak ada:
|
||||
-> buat draft file baru dari template
|
||||
-> copy object template ke key final file baru
|
||||
-> simpan metadata template
|
||||
-> pastikan ada child helicopter_files di section fpwb
|
||||
-> jika child baru dibuat, publish SSE helicopter.file.created
|
||||
-> mint WOPI token
|
||||
-> balikin Collabora action URL
|
||||
```
|
||||
|
||||
Catatan untuk step 9:
|
||||
|
||||
1. SSE dipublish oleh backend App API pada request `create-from-template`
|
||||
2. jadi FE yang sedang membuka halaman helicopter files bisa langsung menerima event tanpa menunggu save dari Collabora
|
||||
3. source of truth event tetap berasal dari proses create child row di backend
|
||||
|
||||
### Step 2 - Frontend simpan response
|
||||
|
||||
Frontend wajib menyimpan:
|
||||
|
||||
1. `file_id`
|
||||
2. `collabora_action`
|
||||
3. `access_token`
|
||||
4. `access_token_ttl` dalam milidetik
|
||||
|
||||
`file_id` dipakai sebagai join key untuk session berikutnya.
|
||||
`file_id` ini juga bisa dikirim lagi saat frontend create takeover lewat field `edited_template_file_ids`.
|
||||
|
||||
Kalau tujuan frontend adalah menampilkan child di helicopter files:
|
||||
|
||||
1. `file_id` adalah ID file manager file
|
||||
2. `helicopter_file_id` tidak ada di response create-from-template
|
||||
3. `helicopter_file_id` akan muncul lewat:
|
||||
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
atau SSE `helicopter.file.created`
|
||||
|
||||
### Step 3 - Frontend buka Collabora
|
||||
|
||||
Frontend tidak boleh hanya redirect atau `window.open(collabora_action)` begitu saja.
|
||||
|
||||
`collabora_action` harus dibuka dengan `POST` form yang ikut mengirim:
|
||||
|
||||
1. `access_token`
|
||||
2. `access_token_ttl` dalam milidetik
|
||||
|
||||
Ini penting karena backend WOPI membaca token dari query `access_token` atau header bearer pada request WOPI yang datang dari Collabora.
|
||||
Kalau frontend hanya membuka `collabora_action` tanpa token bootstrap, backend akan mengembalikan error seperti:
|
||||
|
||||
1. `401 FILE_MANAGER_WOPI_INVALID_TOKEN`
|
||||
2. `token is malformed: token contains an invalid number of segments`
|
||||
|
||||
Frontend membuka editor memakai:
|
||||
|
||||
1. `collabora_action` sebagai `form action`
|
||||
2. `access_token` sebagai hidden input
|
||||
3. `access_token_ttl` sebagai hidden input
|
||||
|
||||
Rule praktis:
|
||||
|
||||
1. jangan pernah memanggil `/wopi/...` dari browser
|
||||
2. jangan pernah expose S3 URL atau credential ke frontend
|
||||
3. gunakan `collabora_action` sebagai base URL editor
|
||||
4. jangan buka `collabora_action` tanpa submit `access_token`
|
||||
|
||||
## File Akan Muncul Di Mana
|
||||
|
||||
Setelah `create-from-template` sukses, hasil akhirnya ada di dua tempat berbeda yang perlu dibedakan:
|
||||
|
||||
### 1) File manager file
|
||||
|
||||
Ini adalah object file utama yang dibuka oleh Collabora.
|
||||
|
||||
Identifier utamanya:
|
||||
|
||||
1. `file_id`
|
||||
|
||||
Endpoint yang relevan:
|
||||
|
||||
1. `GET /api/v1/file-manager/files/{fileId}/edit-session`
|
||||
2. WOPI internal backend `/wopi/files/{fileId}` dan `/wopi/files/{fileId}/contents`
|
||||
|
||||
### 2) Helicopter file child
|
||||
|
||||
Ini adalah row bisnis yang membuat file tadi muncul di halaman helicopter files.
|
||||
|
||||
Lokasinya:
|
||||
|
||||
1. `GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
2. response path `data.attributes.files.prepare`
|
||||
3. section row = `fpwb`
|
||||
|
||||
Field penting item collection:
|
||||
|
||||
1. `helicopter_file_id`
|
||||
2. `attachment.id`
|
||||
3. `attachment.file_name`
|
||||
|
||||
Catatan:
|
||||
|
||||
1. child ini bukan parent folder file manager
|
||||
2. child ini adalah row di tabel `helicopter_files`
|
||||
3. relasinya menunjuk ke file hasil template lewat `source_file_id`
|
||||
|
||||
## Contoh Integrasi Next.js
|
||||
|
||||
Bagian ini menunjukkan contoh yang lengkap untuk Next.js App Router.
|
||||
|
||||
Asumsi:
|
||||
|
||||
1. user sudah login ke App API
|
||||
2. frontend memanggil backend App API, bukan endpoint `/wopi/...`
|
||||
3. backend mengembalikan `file_id`, `collabora_action`, `access_token`, dan `access_token_ttl`
|
||||
|
||||
### Tipe response
|
||||
|
||||
```ts
|
||||
export type CollaboraSessionResponse = {
|
||||
data: {
|
||||
file_id: string
|
||||
collabora_action: string
|
||||
access_token: string
|
||||
access_token_ttl: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server action atau API helper
|
||||
|
||||
Contoh helper untuk memanggil backend `create-from-template`.
|
||||
|
||||
```ts
|
||||
// app/lib/file-manager.ts
|
||||
export type CreateFromTemplateInput = {
|
||||
templateUuid: string
|
||||
helicopterId: string
|
||||
takeoverId?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CollaboraSessionResponse = {
|
||||
data: {
|
||||
file_id: string
|
||||
collabora_action: string
|
||||
access_token: string
|
||||
access_token_ttl: number
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFromTemplate(
|
||||
input: CreateFromTemplateInput,
|
||||
accessToken: string,
|
||||
): Promise<CollaboraSessionResponse> {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/file-manager/files/create-from-template`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "file_manager_file_create_from_template",
|
||||
attributes: {
|
||||
template_uuid: input.templateUuid,
|
||||
helicopter_id: input.helicopterId,
|
||||
takeover_id: input.takeoverId,
|
||||
name: input.name,
|
||||
},
|
||||
},
|
||||
}),
|
||||
cache: "no-store",
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Create from template failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
Contoh helper untuk membuka file existing:
|
||||
|
||||
```ts
|
||||
// app/lib/file-manager.ts
|
||||
export async function getEditSession(
|
||||
fileId: string,
|
||||
accessToken: string,
|
||||
): Promise<CollaboraSessionResponse> {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/file-manager/files/${fileId}/edit-session`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
cache: "no-store",
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Edit session failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
### Komponen client untuk bootstrap Collabora
|
||||
|
||||
Komponen ini membuat form hidden dan auto-submit ke `collabora_action`.
|
||||
|
||||
```tsx
|
||||
// app/components/collabora-launcher.tsx
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
type Props = {
|
||||
collaboraAction: string
|
||||
accessToken: string
|
||||
accessTokenTTL: number // milliseconds
|
||||
target?: string
|
||||
}
|
||||
|
||||
export function CollaboraLauncher({
|
||||
collaboraAction,
|
||||
accessToken,
|
||||
accessTokenTTL,
|
||||
target = "collabora-editor",
|
||||
}: Props) {
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.submit()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<form ref={formRef} action={collaboraAction} method="post" target={target}>
|
||||
<input type="hidden" name="access_token" value={accessToken} />
|
||||
<input type="hidden" name="access_token_ttl" value={String(accessTokenTTL)} />
|
||||
</form>
|
||||
|
||||
<iframe
|
||||
name={target}
|
||||
title="Collabora Editor"
|
||||
className="h-[calc(100vh-120px)] w-full border-0"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Halaman Next.js untuk create from template
|
||||
|
||||
Contoh berikut memakai App Router dan menampilkan editor setelah session berhasil dibuat.
|
||||
|
||||
```tsx
|
||||
// app/file-manager/templates/[templateId]/open/page.tsx
|
||||
import { CollaboraLauncher } from "@/app/components/collabora-launcher"
|
||||
import { createFromTemplate } from "@/app/lib/file-manager"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{
|
||||
templateId: string
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
helicopterId?: string
|
||||
name?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function OpenTemplatePage({ params, searchParams }: PageProps) {
|
||||
const { templateId } = await params
|
||||
const { helicopterId, name } = await searchParams
|
||||
|
||||
if (!helicopterId || !name) {
|
||||
throw new Error("helicopterId and name are required")
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
const accessToken = cookieStore.get("access_token")?.value
|
||||
if (!accessToken) {
|
||||
throw new Error("Missing app access token")
|
||||
}
|
||||
|
||||
const session = await createFromTemplate(
|
||||
{
|
||||
templateUuid: templateId,
|
||||
helicopterId,
|
||||
name,
|
||||
},
|
||||
accessToken,
|
||||
)
|
||||
|
||||
return (
|
||||
<CollaboraLauncher
|
||||
collaboraAction={session.data.collabora_action}
|
||||
accessToken={session.data.access_token}
|
||||
accessTokenTTL={session.data.access_token_ttl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Halaman Next.js untuk file existing
|
||||
|
||||
```tsx
|
||||
// app/file-manager/files/[fileId]/edit/page.tsx
|
||||
import { CollaboraLauncher } from "@/app/components/collabora-launcher"
|
||||
import { getEditSession } from "@/app/lib/file-manager"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{
|
||||
fileId: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default async function EditFilePage({ params }: PageProps) {
|
||||
const { fileId } = await params
|
||||
|
||||
const cookieStore = await cookies()
|
||||
const accessToken = cookieStore.get("access_token")?.value
|
||||
if (!accessToken) {
|
||||
throw new Error("Missing app access token")
|
||||
}
|
||||
|
||||
const session = await getEditSession(fileId, accessToken)
|
||||
|
||||
return (
|
||||
<CollaboraLauncher
|
||||
collaboraAction={session.data.collabora_action}
|
||||
accessToken={session.data.access_token}
|
||||
accessTokenTTL={session.data.access_token_ttl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Kenapa tidak cukup pakai `window.open(collabora_action)`?
|
||||
|
||||
Karena response backend memisahkan:
|
||||
|
||||
1. `collabora_action`
|
||||
2. `access_token`
|
||||
3. `access_token_ttl`
|
||||
|
||||
Sedangkan backend WOPI membutuhkan token valid saat Collabora mulai memanggil:
|
||||
|
||||
1. `GET /wopi/files/{fileId}`
|
||||
2. `GET /wopi/files/{fileId}/contents`
|
||||
|
||||
Kalau frontend hanya membuka URL editor tanpa mengirim hidden form fields di atas, Collabora akan tetap mencoba request WOPI, tetapi backend akan membaca token kosong atau malformed.
|
||||
|
||||
### Contoh yang salah
|
||||
|
||||
```tsx
|
||||
window.open(session.data.collabora_action, "_blank")
|
||||
```
|
||||
|
||||
atau:
|
||||
|
||||
```tsx
|
||||
<iframe src={session.data.collabora_action} />
|
||||
```
|
||||
|
||||
Kedua contoh di atas tidak mengirim `access_token` bootstrap.
|
||||
|
||||
### Contoh yang benar
|
||||
|
||||
```tsx
|
||||
<form action={session.data.collabora_action} method="post" target="collabora-editor">
|
||||
<input type="hidden" name="access_token" value={session.data.access_token} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="access_token_ttl"
|
||||
value={String(session.data.access_token_ttl)}
|
||||
/>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Step 4 - Autosave berjalan di backend
|
||||
|
||||
Setelah editor terbuka:
|
||||
|
||||
1. Collabora memanggil `GET /wopi/files/{fileId}`
|
||||
2. Collabora memanggil `GET /wopi/files/{fileId}/contents`
|
||||
3. saat autosave, Collabora memanggil `POST /wopi/files/{fileId}/contents`
|
||||
4. saat close / exit save, Collabora mengirim lifecycle headers
|
||||
|
||||
Frontend tidak perlu mengurus autosave secara manual.
|
||||
|
||||
## Flow Tampilan Child Di Helicopter Files
|
||||
|
||||
Kalau FE punya halaman detail helicopter files yang memanggil:
|
||||
|
||||
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
|
||||
maka flow lengkapnya begini:
|
||||
|
||||
### Step 1 - Load data awal
|
||||
|
||||
Frontend fetch:
|
||||
|
||||
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
|
||||
Lalu render:
|
||||
|
||||
1. `data.attributes.files.before`
|
||||
2. `data.attributes.files.prepare`
|
||||
3. `data.attributes.files.after`
|
||||
|
||||
Hasil `create-from-template` akan masuk ke array:
|
||||
|
||||
`data.attributes.files.prepare`
|
||||
|
||||
### Step 2 - Subscribe SSE
|
||||
|
||||
Saat halaman helicopter files terbuka, frontend subscribe:
|
||||
|
||||
`GET /api/v1/helicopter-files/events?helicopter_id={helicopter_id}`
|
||||
|
||||
Event yang perlu didengar:
|
||||
|
||||
`helicopter.file.created`
|
||||
|
||||
Penjelasan singkat:
|
||||
|
||||
1. endpoint ini adalah stream SSE dari backend App API
|
||||
2. backend akan keep connection tetap hidup dan mengirim heartbeat
|
||||
3. event akan dikirim ketika backend membuat child `helicopter_file` baru untuk helicopter itu
|
||||
4. event ini tidak tergantung pada autosave WOPI
|
||||
|
||||
Contoh payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"helicopter_id": "019dfb9e-db0c-7eaf-b701-8b4928e62d24",
|
||||
"helicopter_file_id": "019f2000-aaaa-7bbb-8ccc-111111111111",
|
||||
"file_id": "019f2000-dddd-7eee-8fff-222222222222",
|
||||
"section": "fpwb",
|
||||
"position": 3,
|
||||
"is_mandatory": false,
|
||||
"attachment_pending": true,
|
||||
"name": "collabora-draft.docx",
|
||||
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"updated_at": "2026-07-01T08:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 - Append child baru ke state
|
||||
|
||||
Kalau frontend menerima event:
|
||||
|
||||
1. cocokkan `helicopter_id`
|
||||
2. cek `section === "fpwb"`
|
||||
3. append ke collection `prepare`
|
||||
4. hindari duplikasi dengan key `helicopter_file_id`
|
||||
|
||||
Contoh Next.js client hook:
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
type HelicopterFileCreatedEvent = {
|
||||
helicopter_id: string
|
||||
helicopter_file_id: string
|
||||
file_id: string
|
||||
section: string
|
||||
position: number
|
||||
is_mandatory: boolean
|
||||
attachment_pending: boolean
|
||||
name: string
|
||||
mime_type: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type PrepareItem = {
|
||||
helicopter_file_id: string
|
||||
section: string
|
||||
position: number
|
||||
is_mandatory: boolean
|
||||
attachment_pending: boolean
|
||||
attachment: {
|
||||
id: string
|
||||
file_name: string
|
||||
download_url: string
|
||||
}
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export function useHelicopterFileCreatedSSE(
|
||||
helicopterId: string,
|
||||
onCreated: (event: HelicopterFileCreatedEvent) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const url = new URL(
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/helicopter-files/events`,
|
||||
)
|
||||
url.searchParams.set("helicopter_id", helicopterId)
|
||||
|
||||
const source = new EventSource(url.toString(), { withCredentials: true })
|
||||
|
||||
const handler = (raw: MessageEvent<string>) => {
|
||||
const payload = JSON.parse(raw.data) as HelicopterFileCreatedEvent
|
||||
onCreated(payload)
|
||||
}
|
||||
|
||||
source.addEventListener("helicopter.file.created", handler as EventListener)
|
||||
|
||||
return () => {
|
||||
source.removeEventListener(
|
||||
"helicopter.file.created",
|
||||
handler as EventListener,
|
||||
)
|
||||
source.close()
|
||||
}
|
||||
}, [helicopterId, onCreated])
|
||||
}
|
||||
```
|
||||
|
||||
Contoh append ke state:
|
||||
|
||||
```tsx
|
||||
setPrepareFiles((current) => {
|
||||
if (current.some((item) => item.helicopter_file_id === event.helicopter_file_id)) {
|
||||
return current
|
||||
}
|
||||
|
||||
const nextItem = {
|
||||
helicopter_file_id: event.helicopter_file_id,
|
||||
section: event.section,
|
||||
position: event.position,
|
||||
is_mandatory: event.is_mandatory,
|
||||
attachment_pending: event.attachment_pending,
|
||||
attachment: {
|
||||
id: event.file_id,
|
||||
file_name: event.name,
|
||||
download_url: "",
|
||||
},
|
||||
created_at: event.updated_at,
|
||||
updated_at: event.updated_at,
|
||||
}
|
||||
|
||||
return [...current, nextItem].sort((a, b) => a.position - b.position)
|
||||
})
|
||||
```
|
||||
|
||||
Catatan:
|
||||
|
||||
1. saat event baru datang, `attachment_pending` bisa masih `true`
|
||||
2. artinya child row sudah ada, tetapi attachment final mungkin belum ter-resolve penuh
|
||||
3. kalau FE butuh shape yang benar-benar final, aman juga untuk re-fetch sekali ke:
|
||||
`GET /api/v1/helicopter-files/get/{helicopter_id}`
|
||||
|
||||
### Step 5 - Refresh token saat TTL hampir habis
|
||||
|
||||
Token WOPI bersifat short-lived.
|
||||
|
||||
Saat token mendekati habis, frontend harus:
|
||||
|
||||
1. memanggil ulang `GET /api/v1/file-manager/files/{fileId}/edit-session` untuk file yang sudah ada
|
||||
|
||||
Lalu replace token yang dipakai untuk membuka editor.
|
||||
|
||||
Catatan:
|
||||
|
||||
1. setelah file pertama sukses dibuat, jangan panggil `create-from-template` lagi hanya untuk refresh token
|
||||
2. untuk reopen atau refresh editor, gunakan `edit-session`
|
||||
3. `create-from-template` dipakai untuk bootstrap file baru atau reuse by `template + takeover`
|
||||
|
||||
## Flow Edit File Existing
|
||||
|
||||
### Step 1 - Frontend minta edit session
|
||||
|
||||
Frontend hanya mengirim `fileId`.
|
||||
|
||||
Backend akan:
|
||||
|
||||
1. load file node
|
||||
2. cari takeover asal file
|
||||
3. verifikasi user punya akses ke takeover
|
||||
4. mint access token baru
|
||||
5. balikin Collabora action URL
|
||||
|
||||
### Step 2 - Frontend buka editor
|
||||
|
||||
Frontend membuka editor pakai response yang sama seperti flow create-from-template.
|
||||
|
||||
## Lifecycle Yang Perlu Dipahami Frontend
|
||||
|
||||
### Saat editor dibuka
|
||||
|
||||
Backend sudah membuat record file node.
|
||||
|
||||
Artinya:
|
||||
|
||||
1. tidak ada lagi langkah create file saat save
|
||||
2. Collabora tinggal overwrite file yang sama
|
||||
3. `fileId` tetap sama selama sesi itu
|
||||
|
||||
### Saat user mengetik
|
||||
|
||||
Collabora akan autosave ke backend melalui WOPI.
|
||||
|
||||
Frontend tidak perlu memanggil endpoint save sendiri.
|
||||
|
||||
### Saat user menutup editor
|
||||
|
||||
Jika Collabora mengirim exit-save:
|
||||
|
||||
1. backend menandai file `final`
|
||||
2. backend attach relasi takeover bila perlu saat create takeover menerima `edited_template_file_ids`
|
||||
3. frontend cukup refresh data list / detail
|
||||
4. child `fpwb` tetap berada di helicopter files karena row bisnisnya sudah dibuat sejak create-from-template
|
||||
|
||||
### Jika user menutup tanpa perubahan
|
||||
|
||||
File tetap berada di status draft dan bisa dibersihkan oleh job cleanup jika stale.
|
||||
|
||||
## Permission Model
|
||||
|
||||
### App API
|
||||
|
||||
App API tetap memakai session auth normal.
|
||||
|
||||
Frontend harus sudah login sebelum memanggil:
|
||||
|
||||
1. `POST /api/v1/file-manager/files/create-from-template`
|
||||
2. `GET /api/v1/file-manager/files/{fileId}/edit-session`
|
||||
|
||||
### WOPI API
|
||||
|
||||
WOPI API memakai:
|
||||
|
||||
1. `access_token`
|
||||
2. proof key validation
|
||||
3. permission claim `read` atau `write`
|
||||
|
||||
Frontend tidak perlu menambahkan permission manual ke WOPI request. Itu sudah ditangani backend saat mint token.
|
||||
|
||||
## Error Handling Singkat
|
||||
|
||||
Semua error App API mengikuti envelope `apperrorsx` yang sudah dipakai backend:
|
||||
|
||||
1. `status`
|
||||
2. `code`
|
||||
3. `error_code`
|
||||
4. `title`
|
||||
5. `message`
|
||||
|
||||
Untuk frontend, patokan utamanya adalah `code` dan `error_code`, bukan hanya HTTP status.
|
||||
|
||||
Mapping singkat yang dipakai di flow ini:
|
||||
|
||||
1. `403` App API: `FORBIDDEN_ACCESS` / `USER_UNAUTHORIZED` saat user tidak boleh akses takeover atau file.
|
||||
2. `401/403` WOPI: `FILE_MANAGER_WOPI_INVALID_TOKEN`, `FILE_MANAGER_WOPI_INVALID_PROOF`, atau `FILE_MANAGER_WOPI_PERMISSION_DENIED`.
|
||||
3. `409` save WOPI: `FILE_MANAGER_WOPI_LOCK_CONFLICT` atau `FILE_MANAGER_WOPI_STALE_TIMESTAMP`.
|
||||
4. `422` input invalid: `VALIDATION_ERROR` atau `FILE_MANAGER_WOPI_VALIDATION_FAILED`.
|
||||
5. `404` file/takeover missing: `DATA_NOT_FOUND`, `FILE_MANAGER_FILE_NOT_FOUND`, atau `FILE_MANAGER_WOPI_FILE_NOT_FOUND`.
|
||||
|
||||
Catatan:
|
||||
|
||||
1. WOPI save conflict kadang tetap mengembalikan body protokol khusus seperti `COOLStatusCode: 1010`.
|
||||
2. Untuk WOPI, frontend tetap tidak memanggil endpoint tersebut langsung; yang perlu di-handle adalah response dari backend saat membuka editor.
|
||||
|
||||
## Hal Yang Harus Dihindari
|
||||
|
||||
1. Jangan panggil `/wopi/...` langsung dari frontend.
|
||||
2. Jangan kirim credential S3 ke browser.
|
||||
3. Jangan hardcode file path di frontend.
|
||||
4. Jangan anggap token WOPI berlaku lama.
|
||||
5. Jangan reuse token file A untuk file B.
|
||||
6. Jangan pakai `create-from-template` berulang-ulang hanya untuk refresh editor file yang sama.
|
||||
241
docs/file-manager-delete-store.md
Normal file
241
docs/file-manager-delete-store.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# File Manager
|
||||
|
||||
# Delete, Restore, Purge
|
||||
|
||||
## Ringkasan Endpoint Utama
|
||||
|
||||
1. `DELETE /api/v1/file-manager/delete` untuk soft delete bulk (file/folder)
|
||||
2. `PATCH /api/v1/file-manager/restore` untuk restore bulk (file/folder) ke lokasi asal
|
||||
3. `DELETE /api/v1/file-manager/purge-delete` untuk hard delete bulk (file/folder)
|
||||
4. `GET /api/v1/file-manager/trash/get-all` untuk list trash merged
|
||||
|
||||
## Catatan Kontrak
|
||||
|
||||
1. Tidak ada endpoint delete single.
|
||||
2. Tidak ada endpoint restore single.
|
||||
3. Gunakan endpoint bulk untuk delete/restore/purge.
|
||||
|
||||
## Step 1 - Soft Delete Node (Bulk)
|
||||
|
||||
### Endpoint
|
||||
- `DELETE /api/v1/file-manager/delete`
|
||||
|
||||
### Tujuan
|
||||
- soft delete file/folder (masuk trash)
|
||||
- jika folder di-delete, subtree (child folder + child file) ikut ke-trash
|
||||
|
||||
### Contoh request
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "uuid": "folder-uuid" },
|
||||
{ "uuid": "file-uuid" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_delete_result",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"status": "trashed"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_delete_result",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"status": "trashed"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 2,
|
||||
"success": 2,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3 - Lihat Trash
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/trash/get-all`
|
||||
|
||||
### Tujuan
|
||||
- menampilkan data trash merged (folder + file)
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_trash_node",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"name": "Documents",
|
||||
"path_cache": "/Documents",
|
||||
"trashed_at": "2026-04-01T10:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file_manager_trash_node",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"name": "LOGO.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 100988,
|
||||
"mime_type": "image/png",
|
||||
"trashed_at": "2026-04-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"page_number": 1,
|
||||
"page_size": 20,
|
||||
"folders": 1,
|
||||
"files": 1,
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4 - Restore Node (Bulk, kembali ke lokasi asal)
|
||||
|
||||
### Endpoint
|
||||
- `PATCH /api/v1/file-manager/restore`
|
||||
|
||||
### Contoh request
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [{ "uuid": "folder-uuid" }, { "uuid": "file-uuid" }]
|
||||
}
|
||||
```
|
||||
|
||||
Aturan:
|
||||
- restore tidak menerima `destination_folder_uuid` lagi
|
||||
- node akan kembali ke lokasi asal (parent/folder saat sebelum di-trash)
|
||||
- jika lokasi asal root, field `location` tidak dimunculkan
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_restore_result",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"status": "ready"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_restore_result",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"status": "ready",
|
||||
"location": {
|
||||
"folder_id": "folder-uuid-optional"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 2,
|
||||
"success": 2,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5 - Purge Node (Hard Delete)
|
||||
|
||||
### Endpoint
|
||||
- `DELETE /api/v1/file-manager/purge-delete` (bulk)
|
||||
|
||||
### Tujuan
|
||||
- hapus permanen file atau folder (UUID bisa keduanya)
|
||||
- kalau UUID folder: subtree ikut dihapus permanen
|
||||
- kalau UUID file: file dihapus permanen
|
||||
|
||||
### Contoh request bulk
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "uuid": "folder-uuid" },
|
||||
{ "uuid": "file-uuid" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_purge_result",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"status": "purged"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_purge_result",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"status": "purged"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 2,
|
||||
"success": 2,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
230
docs/file-manager-get.md
Normal file
230
docs/file-manager-get.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# File Manager - GET Data (Step by Step)
|
||||
|
||||
# Daftar Endpoint
|
||||
## Ringkasan Endpoint Utama
|
||||
1. `GET /file-manager/get-all` untuk explorer merged (root/child)
|
||||
2. `GET /file-manager/trash/get-all` untuk trash merged
|
||||
3. `GET /file-manager/files/get/:uuid` untuk detail file
|
||||
4. `GET /file-manager/folders/get/:uuid` untuk detail folder
|
||||
|
||||
## Endpoint Removed (tidak tersedia lagi)
|
||||
1. `GET /file-manager/files/get-all`
|
||||
2. `GET /file-manager/folders/get-all`
|
||||
3. `GET /file-manager/files/get-all-trash`
|
||||
4. `GET /file-manager/folders/get-all-trash`
|
||||
|
||||
## Step 1 - Ambil Explorer Root
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/get-all`
|
||||
|
||||
### Tujuan
|
||||
- menampilkan isi root explorer (merged folder + file).
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_node",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"name": "Documents",
|
||||
"status": "ready",
|
||||
"path_cache": "/Documents",
|
||||
"created_at": "2026-03-31T08:52:03Z",
|
||||
"updated_at": "2026-03-31T08:52:03Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file_manager_node",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"name": "LOGO.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 100988,
|
||||
"mime_type": "image/png",
|
||||
"download_url": "https://...",
|
||||
"status": "ready",
|
||||
"created_at": "2026-03-31T09:30:31Z",
|
||||
"updated_at": "2026-03-31T09:30:31Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"is_root": true,
|
||||
"folders": 1,
|
||||
"files": 1,
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2 - Buka Isi Folder
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/get-all?parent_uuid=<folder_uuid>`
|
||||
|
||||
### Tujuan
|
||||
- menampilkan child langsung dari folder yang dipilih.
|
||||
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_node",
|
||||
"id": "inside-file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"parent_id": "folder-uuid",
|
||||
"name": "inside-folder.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 42170,
|
||||
"mime_type": "image/png",
|
||||
"download_url": "https://...",
|
||||
"status": "ready",
|
||||
"created_at": "2026-04-05T07:29:33Z",
|
||||
"updated_at": "2026-04-05T07:33:44Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"is_root": false,
|
||||
"parent_uuid": "folder-uuid",
|
||||
"breadcrumb": [
|
||||
{
|
||||
"id": "folder-uuid-level-1",
|
||||
"name": "Documents"
|
||||
},
|
||||
{
|
||||
"id": "folder-uuid",
|
||||
"name": "Project A"
|
||||
}
|
||||
],
|
||||
"folders": 0,
|
||||
"files": 1,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2b - Ambil Data Trash (Merged)
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/trash/get-all`
|
||||
|
||||
### Tujuan
|
||||
- menampilkan daftar node yang sedang trashed (folder + file).
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_trash_node",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"node_type": "folder",
|
||||
"name": "Documents",
|
||||
"path_cache": "/Documents",
|
||||
"trashed_at": "2026-04-01T10:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file_manager_trash_node",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"node_type": "file",
|
||||
"name": "logo.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 42170,
|
||||
"mime_type": "image/png",
|
||||
"download_url": "https://...",
|
||||
"trashed_at": "2026-04-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3 - Ambil Detail File
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/files/get/:uuid`
|
||||
|
||||
### Tujuan
|
||||
- ambil detail information dari satu file untuk panel/detail page.
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_file",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"name": "spidey.png",
|
||||
"name_normalized": "spidey.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 42170,
|
||||
"mime_type": "image/png",
|
||||
"download_url": "https://...",
|
||||
"status": "ready",
|
||||
"location": {
|
||||
"folder_id": "folder-uuid"
|
||||
},
|
||||
"processing": {
|
||||
"processing_started_at": "2026-04-05T07:29:34Z",
|
||||
"processed_at": "2026-04-05T07:29:34Z",
|
||||
"validated_at": "2026-04-05T07:29:34Z"
|
||||
},
|
||||
"audit": {
|
||||
"created_at": "2026-04-05T07:29:33Z",
|
||||
"created_by": "019d194e-f595-7d5f-a5bf-3609231ba43d",
|
||||
"updated_at": "2026-04-05T07:33:44Z",
|
||||
"updated_by": "019d194e-f595-7d5f-a5bf-3609231ba43d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4 - Ambil Detail Folder
|
||||
|
||||
### Endpoint
|
||||
- `GET /api/v1/file-manager/folders/get/:uuid`
|
||||
|
||||
### Tujuan
|
||||
- ambil detail satu folder (mis. untuk panel detail folder/breadcrumb context).
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_folder",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"name": "Documents",
|
||||
"name_normalized": "documents",
|
||||
"path_cache": "/Documents",
|
||||
"audit": {
|
||||
"created_at": "2026-03-31T08:52:03Z",
|
||||
"created_by": "019d194e-f595-7d5f-a5bf-3609231ba43d",
|
||||
"updated_at": "2026-03-31T08:52:03Z",
|
||||
"updated_by": "019d194e-f595-7d5f-a5bf-3609231ba43d"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
207
docs/file-manager-preview.md
Normal file
207
docs/file-manager-preview.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# File Manager — Document Preview
|
||||
|
||||
Fitur ini memungkinkan FE untuk menampilkan preview dokumen (PDF, DOCX, XLSX, PPTX) secara inline tanpa user perlu mendownload file terlebih dahulu.
|
||||
|
||||
---
|
||||
|
||||
## Cara Kerja
|
||||
|
||||
### Untuk File PDF
|
||||
|
||||
PDF langsung dapat dirender oleh browser. Backend mengembalikan `preview_url` yang menunjuk ke file aslinya (sama dengan `download_url`). Tidak ada proses konversi.
|
||||
|
||||
### Untuk File Office (DOCX, XLSX, PPTX, DOC, XLS, PPT)
|
||||
|
||||
Dokumen dikonversi ke PDF secara otomatis oleh background worker saat file selesai diupload. Proses ini menggunakan **Gotenberg** (container yang menjalankan LibreOffice secara headless). Hasil konversi disimpan di S3 sebagai file terpisah (`.preview.pdf`).
|
||||
|
||||
### Untuk File Lainnya (Image, Video, dll)
|
||||
|
||||
`preview_url` tidak diisi (kosong). FE menangani preview image menggunakan `download_url` seperti sebelumnya.
|
||||
|
||||
---
|
||||
|
||||
## Flow Lengkap
|
||||
|
||||
```
|
||||
1. FE upload file (existing flow — tidak berubah)
|
||||
|
||||
2. Background worker memproses file:
|
||||
- Image → generate thumbnail (existing)
|
||||
- PDF → tidak perlu konversi, preview_url = original URL
|
||||
- Office → konversi ke PDF via Gotenberg → simpan preview di S3 → update DB
|
||||
|
||||
3. File status menjadi "ready"
|
||||
|
||||
4. FE ambil detail file:
|
||||
GET /api/v1/file-manager/files/get/{uuid}
|
||||
→ response berisi preview_url
|
||||
|
||||
5. FE render preview_url di iframe/tab baru
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoint
|
||||
|
||||
### GET /api/v1/file-manager/files/get/{uuid}
|
||||
|
||||
Endpoint existing. Response sekarang memiliki field tambahan `preview_url` di dalam `attributes`.
|
||||
|
||||
#### Contoh Response (PDF atau Office doc yang sudah diproses)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_file",
|
||||
"id": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"attributes": {
|
||||
"name": "laporan.docx",
|
||||
"extension": "docx",
|
||||
"size_bytes": 204800,
|
||||
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"status": "ready",
|
||||
"download_url": "https://s3.../laporan.docx?X-Amz-Signature=...",
|
||||
"preview_url": "https://s3.../laporan.preview.pdf?X-Amz-Signature=...",
|
||||
"audit": {
|
||||
"created_at": "2026-07-06T10:00:00Z",
|
||||
"updated_at": "2026-07-06T10:00:05Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Contoh Response (file masih diproses atau konversi gagal)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_file",
|
||||
"id": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"attributes": {
|
||||
"name": "laporan.docx",
|
||||
"status": "processing",
|
||||
"download_url": "https://s3.../laporan.docx?X-Amz-Signature=...",
|
||||
"preview_url": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logika `preview_url` di Backend
|
||||
|
||||
| Kondisi | `preview_url` |
|
||||
|---------|--------------|
|
||||
| File PDF | Sama dengan `download_url` |
|
||||
| File Office, sudah dikonversi | Presigned URL ke `.preview.pdf` di S3 |
|
||||
| File Office, belum/gagal dikonversi | Kosong (`""`) |
|
||||
| File image / tipe lainnya | Kosong (`""`) |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Jika konversi via Gotenberg gagal:
|
||||
|
||||
- **Error 4xx dari Gotenberg** (dokumen tidak valid/tidak didukung): file status menjadi `failed`. FE harus handle kondisi ini dengan menampilkan pesan error atau fallback ke tombol download.
|
||||
- **Error jaringan atau 5xx dari Gotenberg** (Gotenberg sedang down): worker akan retry otomatis. File status tetap `processing` sampai berhasil atau melebihi batas retry.
|
||||
|
||||
---
|
||||
|
||||
## Panduan untuk FE
|
||||
|
||||
```js
|
||||
const file = await api.getFile(uuid)
|
||||
const { preview_url, download_url, status, mime_type } = file.data.attributes
|
||||
|
||||
if (preview_url) {
|
||||
// Render preview di iframe
|
||||
openPreviewModal(preview_url)
|
||||
} else if (status === 'processing') {
|
||||
// Tampilkan loading / tunggu SSE event
|
||||
showProcessingIndicator()
|
||||
} else if (status === 'failed') {
|
||||
// Tampilkan error, tawarkan download langsung
|
||||
showError('Preview tidak tersedia')
|
||||
offerDownload(download_url)
|
||||
} else {
|
||||
// File type tidak didukung preview (video, archive, dll)
|
||||
offerDownload(download_url)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cara Test di Local (Tanpa Frontend)
|
||||
|
||||
### Prasyarat
|
||||
|
||||
1. Gotenberg harus jalan:
|
||||
```bash
|
||||
docker-compose up gotenberg
|
||||
```
|
||||
|
||||
2. File worker harus jalan:
|
||||
```bash
|
||||
docker-compose up file-worker
|
||||
```
|
||||
|
||||
3. Pastikan `.env.local` sudah ada `GOTENBERG_URL=http://localhost:3000`
|
||||
|
||||
### Langkah Test
|
||||
|
||||
```bash
|
||||
# 1. Upload file DOCX (step upload + create seperti biasa)
|
||||
|
||||
# 2. Tunggu file status menjadi "ready" (cek via SSE atau polling GET)
|
||||
|
||||
# 3. Ambil detail file
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
http://localhost:8080/api/v1/file-manager/files/get/<uuid>
|
||||
|
||||
# 4. Salin nilai preview_url dari response
|
||||
|
||||
# 5. Buka preview_url di browser — PDF harus tampil
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konfigurasi
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
ports:
|
||||
- "3000:3000"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### .env.local
|
||||
|
||||
```bash
|
||||
GOTENBERG_URL=http://localhost:3000 # local development
|
||||
# GOTENBERG_URL=http://gotenberg:3000 # dalam docker-compose network
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File yang Diubah (OP-1273)
|
||||
|
||||
| File | Perubahan |
|
||||
|------|-----------|
|
||||
| `migrations/20260706_file_preview_metadata.sql` | ALTER TABLE tambah kolom `preview_*` |
|
||||
| `docker-compose.yml` | Tambah service Gotenberg |
|
||||
| `.env.local.example` | Tambah `GOTENBERG_URL` |
|
||||
| `config/local/config.json` | Tambah section `gotenberg.url` |
|
||||
| `internal/config/config.go` | Tambah struct `GotenbergConfig` |
|
||||
| `internal/config/loader.go` | Mapping config Gotenberg |
|
||||
| `internal/domain/file_manager/model_file.go` | Tambah field `Preview*` di struct `File` |
|
||||
| `internal/service/file_processing_usecase.go` | Tambah fungsi `convertDocumentIfNeeded`, `callGotenberg`, helper |
|
||||
| `cmd/worker/file_worker.go` | Wire up `GotenbergURL` ke use case |
|
||||
| `internal/transport/http/dto/shared/file_manager_file_shared.go` | Tambah field `PreviewURL` di `FileManagerFileDetailAttributes` |
|
||||
| `internal/transport/http/handlers/file_manager_request_helpers.go` | Tambah `PreviewURL` ke `fileManagerDownloadURLs` dan logic presign |
|
||||
| `internal/transport/http/handlers/file_manager_file_handler.go` | Update `fileManagerFileDetailResource` untuk set `PreviewURL` |
|
||||
80
docs/file-manager-update-move.md
Normal file
80
docs/file-manager-update-move.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# File Manager
|
||||
|
||||
# Update dan Move (Step by Step)
|
||||
|
||||
## A. Update File
|
||||
|
||||
### Endpoint
|
||||
- `PATCH /api/v1/file-manager/files/update/:uuid`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_file_update",
|
||||
"id": "file-uuid",
|
||||
"attributes": {
|
||||
"name": "spidey-v2.png",
|
||||
"folder_id": "folder-uuid-optional"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Aturan:
|
||||
- `data.id` harus sama dengan `:uuid` di path
|
||||
- minimal isi salah satu: `name` atau `folder_id`
|
||||
- `folder_id` null/kosong bisa dipakai untuk pindah ke root
|
||||
|
||||
## B. Update Folder
|
||||
|
||||
### Endpoint
|
||||
- `PATCH /api/v1/file-manager/folders/update/:uuid`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "file_manager_folder_update",
|
||||
"id": "folder-uuid",
|
||||
"attributes": {
|
||||
"name": "Manuals Updated",
|
||||
"parent_id": "parent-folder-uuid-optional"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Aturan:
|
||||
- `data.id` harus sama dengan `:uuid`
|
||||
- minimal isi salah satu: `name` atau `parent_id`
|
||||
|
||||
## C. Move Node (File/Folder) - Bulk
|
||||
|
||||
### Endpoint
|
||||
- `PATCH /api/v1/file-manager/move`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"uuid": "node-uuid-file-atau-folder",
|
||||
"destination_folder_uuid": "folder-uuid-optional"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Aturan:
|
||||
- `destination_folder_uuid` null/kosong = pindah ke root
|
||||
- `uuid` bisa folder UUID atau file UUID
|
||||
- bisa kirim banyak item sekaligus (bulk)
|
||||
|
||||
## Ringkasan
|
||||
1. `PATCH /files/update/:uuid` untuk rename/move umum file
|
||||
2. `PATCH /folders/update/:uuid` untuk rename/move umum folder
|
||||
3. `PATCH /file-manager/move` untuk move-only file/folder (bulk, boleh 1 item)
|
||||
288
docs/file-manager-upload.md
Normal file
288
docs/file-manager-upload.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# File Manager
|
||||
|
||||
# Uploading New File
|
||||
Sebelum hit API, FE ambil data per file:
|
||||
- `name` (contoh: `spidey.png`)
|
||||
- `content_type` (contoh: `image/png`)
|
||||
- `size_bytes` (contoh: `42170`)
|
||||
|
||||
## Step 1 - FE minta upload intent ke BE, lalu upload langsung ke storage
|
||||
|
||||
## A. Minta Upload Intent ke Backend
|
||||
|
||||
### Endpoint
|
||||
- `POST /api/v1/file-manager/files/upload`
|
||||
|
||||
### FE kirim request ini
|
||||
- Single File
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"attributes": {
|
||||
"name": "image.png",
|
||||
"content_type": "image/png",
|
||||
"size_bytes": 123
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Bulk File
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"attributes": {
|
||||
"name": "image1.png",
|
||||
"content_type": "image/png",
|
||||
"size_bytes": 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"attributes": {
|
||||
"name": "image2.png",
|
||||
"content_type": "image/png",
|
||||
"size_bytes": 456
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Backend akan balas per file
|
||||
- `upload_intent_uuid`
|
||||
- `upload_url`
|
||||
- `method` (`PUT`)
|
||||
- `required_headers`
|
||||
- `object.bucket`
|
||||
- `object.key`
|
||||
- `expires_at`
|
||||
- `expires_in_seconds`
|
||||
|
||||
Contoh response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"id": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"attributes": {
|
||||
"upload_intent_uuid": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"upload_url": "http://localhost:4566/...",
|
||||
"method": "PUT",
|
||||
"required_headers": {
|
||||
"content-type": "image/png"
|
||||
},
|
||||
"object": {
|
||||
"bucket": "wucher-file-dev",
|
||||
"key": "fm/objects/files/2026/04/12/019d8215c6227a1aa20fc21f99f36584.png"
|
||||
},
|
||||
"file": {
|
||||
"name": "spidey.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 42170
|
||||
},
|
||||
"expires_at": "2026-04-12T14:40:58Z",
|
||||
"expires_in_seconds": 899
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 1,
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## B. FE Handle langsung Upload Binary Langsung ke S3
|
||||
|
||||
Untuk setiap item `success: true` dari step 1:
|
||||
1. Ambil `upload_url`
|
||||
2. Ambil `method`
|
||||
3. Ambil `required_headers` (jika ada)
|
||||
4. Lakukan request upload file ke S3
|
||||
|
||||
Contoh yang akan dilakukan FE:
|
||||
|
||||
```js
|
||||
await fetch(upload_url, {
|
||||
method: method, // "PUT"
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
```
|
||||
|
||||
atau pseudo
|
||||
```js
|
||||
const intents = await api.requestUploadIntents(files) // bulk
|
||||
const readyToUpload = intents.data.filter(i => i.success)
|
||||
|
||||
const uploadResults = await parallelLimit(readyToUpload, 4, putToS3) // paralel
|
||||
const succeeded = uploadResults.filter(r => r.success)
|
||||
|
||||
if (succeeded.length > 0) {
|
||||
await api.createFilesBulk(succeeded.map(toCreatePayload))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Catatan penting:
|
||||
- **1 file = 1 PUT ke S3**
|
||||
- boleh paralel (disarankan pakai concurrency limit)
|
||||
- file yang gagal PUT **jangan** dikirim ke step create
|
||||
|
||||
|
||||
## Step 2 - Upload Cancel (opsional)
|
||||
|
||||
### Endpoint
|
||||
- `DELETE /api/v1/file-manager/files/cancel-upload`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_cancel_upload",
|
||||
"attributes": {
|
||||
"upload_intent_uuid": "019d8215-c622-7576-86bd-3ddeecaf4867"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Kapan dipakai
|
||||
- user sudah request upload intent
|
||||
- tapi user batal sebelum create
|
||||
- FE bisa cancel upload intent supaya flow lebih bersih
|
||||
|
||||
### Contoh response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_file_cancel_upload_result",
|
||||
"id": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"attributes": {
|
||||
"status": "canceled"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 1,
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3 - FE Panggil Create ke Backend (Finalisasi)
|
||||
|
||||
Setelah PUT S3 sukses, FE gunakan:
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `POST /api/v1/file-manager/files/create`
|
||||
|
||||
### FE kirim request ini
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_create",
|
||||
"attributes": {
|
||||
"upload_intent_uuid": "019d8215-c622-7576-86bd-3ddeecaf4867",
|
||||
"name": "spidey.png",
|
||||
"folder_id": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Aturan:
|
||||
|
||||
- `upload_intent_uuid`: wajib
|
||||
- `name`: wajib
|
||||
- `folder_id`: opsional (kalau mau dimasukkan ke folder)
|
||||
- `null` atau tidak dikirim = root
|
||||
|
||||
## Step 4 - Process Create (BE)
|
||||
|
||||
Untuk setiap item:
|
||||
1. Ambil upload intent dari `upload_intent_uuid`
|
||||
2. Verify object benar-benar ada di storage
|
||||
3. Verify metadata object (size/content type) sesuai intent
|
||||
4. Simpan metadata file ke DB
|
||||
5. Simpan outbox durable
|
||||
6. Set status awal file menjadi `uploaded`
|
||||
|
||||
## Step 5 - Proses Async Lanjutan (Backend)
|
||||
|
||||
Setelah status `uploaded`, worker akan lanjut:
|
||||
|
||||
- `uploaded -> processing -> validated -> ready`
|
||||
- atau `uploaded -> processing -> failed`
|
||||
|
||||
## Step 6 - FE Handle Response Create
|
||||
|
||||
Response create adalah bulk per item.
|
||||
|
||||
- Kalau semua sukses: HTTP `201`
|
||||
- Kalau campuran sukses + gagal: HTTP `207` dengan `partial_success: true`
|
||||
|
||||
Contoh sukses:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"message": "created",
|
||||
"resource": {
|
||||
"type": "file_manager_file",
|
||||
"id": "019d628a-8d92-7949-b355-95de506da3b7",
|
||||
"attributes": {
|
||||
"folder_id": "019d4309-4dc8-78be-bb0c-3ebb8f3056ba",
|
||||
"name": "spidey01.png",
|
||||
"extension": "png",
|
||||
"size_bytes": 42170,
|
||||
"mime_type": "image/png",
|
||||
"status": "uploaded",
|
||||
"created_at": "2026-04-06T11:25:40Z",
|
||||
"updated_at": "2026-04-06T11:25:40Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 1,
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"partial_success": false
|
||||
}
|
||||
}
|
||||
```
|
||||
260
docs/flight-ops-locks-and-usage-sync.md
Normal file
260
docs/flight-ops-locks-and-usage-sync.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Flight Ops Locks and Helicopter Usage Sync
|
||||
|
||||
Dokumen ini menjelaskan alur data untuk `takeover`, `mission`, `flight_data`, `after_flight_inspection`, `fm_report`, dan `helicopter_usage`.
|
||||
|
||||
Tujuan utamanya:
|
||||
- mencegah perubahan data pada fase yang sudah final
|
||||
- menjaga `helicopter_usage` sebagai ringkasan yang dihitung dari data sumber
|
||||
- membuat perilaku backend konsisten untuk frontend dan integrasi lain
|
||||
|
||||
## Ringkasan Aturan
|
||||
|
||||
1. Saat `after_flight_inspection` sudah dibuat:
|
||||
- `mission` untuk flight itu tidak boleh dibuat atau di-update lagi
|
||||
- `flight_data` baru tidak boleh dibuat lagi
|
||||
- `flight_data` yang sudah ada masih boleh diedit
|
||||
2. Saat `fm_report` di-complete:
|
||||
- `flight_data` untuk flight itu terkunci
|
||||
- `fm_report` menjadi read-only
|
||||
- `helicopter_usage` baru dihitung ulang dari data sumber
|
||||
3. Saat `GET /api/v1/fm-reports/get/:flight_id`:
|
||||
- backend tetap mengirim `helicopter_usage`
|
||||
- jika row belum ada di database, response tetap mengirim nilai `0`
|
||||
|
||||
## Alur Besar
|
||||
|
||||
Urutan normalnya:
|
||||
|
||||
1. `takeover` dibuat
|
||||
2. `mission` dibuat
|
||||
3. `flight_data` diisi dan masih bisa diubah
|
||||
4. `after_flight_inspection` dibuat
|
||||
5. `fm_report` draft otomatis tersedia
|
||||
6. user melengkapi `fm_report`
|
||||
7. `POST /api/v1/fm-reports/complete/:flight_id`
|
||||
8. backend menghitung ulang `helicopter_usage`
|
||||
|
||||
## Detail Per Modul
|
||||
|
||||
### 1) Mission
|
||||
|
||||
Mission bisa dibuat dan di-update selama flight belum masuk fase after flight inspection.
|
||||
|
||||
Begitu `after_flight_inspection` sudah ada untuk flight tersebut:
|
||||
- create mission ditolak
|
||||
- update mission ditolak
|
||||
|
||||
Alasan bisnis:
|
||||
- setelah after flight masuk, mission dianggap sudah masuk fase final
|
||||
- mission tidak boleh berubah karena akan mempengaruhi rekonsiliasi data flight
|
||||
|
||||
Error yang biasanya muncul:
|
||||
- `MISSION_AFTER_FLIGHT_LOCKED`
|
||||
|
||||
Referensi:
|
||||
- lihat tabel `Mission` di [`docs/error-catalog.md`](./error-catalog.md)
|
||||
|
||||
### 2) Flight Data
|
||||
|
||||
`flight_data` adalah sumber utama untuk hitungan operasional seperti:
|
||||
- landing count
|
||||
- durasi flight
|
||||
- hook release
|
||||
- rotor brake cycle
|
||||
|
||||
#### Sebelum after flight inspection
|
||||
|
||||
- create flight data boleh
|
||||
- update flight data boleh
|
||||
- delete flight data boleh
|
||||
|
||||
#### Setelah after flight inspection dibuat
|
||||
|
||||
- create flight data baru ditolak
|
||||
- update flight data yang sudah ada masih boleh
|
||||
- delete flight data yang sudah ada ditolak
|
||||
|
||||
#### Setelah FM report di-complete
|
||||
|
||||
- update flight data ditolak
|
||||
- delete flight data ditolak
|
||||
- data dianggap final
|
||||
|
||||
Error yang biasanya muncul:
|
||||
- `FLIGHT_DATA_CREATE_BLOCKED`
|
||||
- `FLIGHT_DATA_AFTER_FLIGHT_LOCKED`
|
||||
- `FLIGHT_DATA_LOCKED`
|
||||
|
||||
Referensi:
|
||||
- lihat tabel `Flight Data` di [`docs/error-catalog.md`](./error-catalog.md)
|
||||
|
||||
### 3) After Flight Inspection
|
||||
|
||||
`after_flight_inspection` dipakai sebagai penanda bahwa flight sudah masuk fase akhir sebelum FM report di-complete.
|
||||
|
||||
Catatan penting:
|
||||
- create / upsert after flight inspection tidak lagi memicu kalkulasi `helicopter_usage`
|
||||
- after flight inspection hanya menyimpan hasil inspeksi
|
||||
- kalkulasi usage dipindahkan ke langkah `fm_report complete`
|
||||
|
||||
### 4) FM Report
|
||||
|
||||
FM report dibuat sebagai draft setelah after flight inspection masuk ke flow.
|
||||
|
||||
#### Sebelum complete
|
||||
|
||||
- report masih bisa di-update
|
||||
- engine metric masih bisa diisi / diubah
|
||||
- `helicopter_usage` belum dihitung ulang oleh backend
|
||||
|
||||
#### Setelah complete
|
||||
|
||||
- report menjadi read-only
|
||||
- update FM report ditolak
|
||||
- `completed_at` dan `completed_by` disimpan
|
||||
- backend memicu refresh `helicopter_usage`
|
||||
|
||||
Endpoint yang dipakai:
|
||||
- `POST /api/v1/fm-reports/complete/:flight_id`
|
||||
|
||||
Error yang biasanya muncul:
|
||||
- `FM_REPORT_LOCKED`
|
||||
- `FM_REPORT_COMPLETE_FAILED`
|
||||
|
||||
Referensi:
|
||||
- lihat tabel `FM Report` di [`docs/error-catalog.md`](./error-catalog.md)
|
||||
|
||||
### 5) Helicopter Usage
|
||||
|
||||
`helicopter_usage` sekarang diperlakukan sebagai summary record.
|
||||
|
||||
Karakteristiknya:
|
||||
- dihitung dari data sumber
|
||||
- bukan angka yang diisi manual untuk flow normal
|
||||
- tetap bisa di-maintain manual lewat CRUD admin/backfill kalau diperlukan
|
||||
|
||||
#### Sumber data kalkulasi
|
||||
|
||||
- `total_landing` = sum `flight_data.landing_count`
|
||||
- `total_airframe_hours` = sum durasi flight
|
||||
- `total_airframe_cycles` = count flight data
|
||||
- `total_hook_release` = sum `flight_data.hook_releases`
|
||||
- `total_rotor_brake_cycle` = sum `flight_data.rotor_brake_cycle`
|
||||
- `total_flight_report` = count report yang punya helicopter terkait
|
||||
- metric engine diambil dari field FM report dan dijumlahkan
|
||||
|
||||
#### Kapan dihitung
|
||||
|
||||
Kalkulasi ulang dilakukan saat:
|
||||
- `fm_report` di-complete
|
||||
|
||||
Kalkulasi tidak dilakukan lagi saat:
|
||||
- create / update after flight inspection
|
||||
- create / update / delete flight data
|
||||
- draft FM report di-upsert
|
||||
|
||||
## Response `GET FM Report`
|
||||
|
||||
Endpoint:
|
||||
- `GET /api/v1/fm-reports/get/:flight_id`
|
||||
|
||||
Response akan selalu mengirim field `helicopter_usage`.
|
||||
|
||||
Perilaku response:
|
||||
- jika row `helicopter_usage` ada di database, isi nilainya dari row tersebut
|
||||
- jika row belum ada, backend tetap mengirim object `helicopter_usage` dengan semua angka `0`
|
||||
|
||||
Contoh bentuk response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "fm_report",
|
||||
"id": "report-id",
|
||||
"attributes": {
|
||||
"flight_id": "flight-id",
|
||||
"completed_at": null,
|
||||
"helicopter_usage": {
|
||||
"type": "helicopter_usage",
|
||||
"id": "",
|
||||
"attributes": {
|
||||
"helicopter_id": "helicopter-id",
|
||||
"total_landing": 0,
|
||||
"total_airframe_hours": 0,
|
||||
"total_airframe_cycles": 0,
|
||||
"total_engine_1_hours": 0,
|
||||
"total_engine_1_gpc_ng_n1": 0,
|
||||
"total_engine_1_ptc_nf_n2": 0,
|
||||
"total_engine_1_ccc": 0,
|
||||
"total_engine_2_hours": 0,
|
||||
"total_engine_2_gpc_ng_n1": 0,
|
||||
"total_engine_2_ptc_nf_n2": 0,
|
||||
"total_engine_2_ccc": 0,
|
||||
"total_flight_report": 0,
|
||||
"total_hook_release": 0,
|
||||
"total_rotor_brake_cycle": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Status yang Perlu Diingat
|
||||
|
||||
### Masih editable
|
||||
|
||||
- mission sebelum after flight inspection ada
|
||||
- flight data sebelum FM report di-complete
|
||||
- FM report sebelum complete
|
||||
|
||||
### Sudah locked
|
||||
|
||||
- mission setelah after flight inspection ada
|
||||
- flight data setelah FM report di-complete
|
||||
- FM report setelah complete
|
||||
|
||||
## Dampak ke Frontend
|
||||
|
||||
Frontend sebaiknya mengikuti aturan ini:
|
||||
|
||||
1. Jika after flight inspection sudah ada:
|
||||
- sembunyikan atau disable aksi create mission
|
||||
- sembunyikan atau disable aksi create flight data baru
|
||||
2. Jika FM report sudah complete:
|
||||
- disable semua edit flight data
|
||||
- disable edit FM report
|
||||
3. Saat menampilkan FM report:
|
||||
- selalu render section helicopter usage
|
||||
- jika angka semua `0`, itu berarti belum ada summary yang terhitung atau belum ada data sumber yang selesai
|
||||
|
||||
## Error yang Relevan
|
||||
|
||||
Lihat katalog lengkap di:
|
||||
- [`docs/error-catalog.md`](./error-catalog.md)
|
||||
|
||||
Error yang paling sering dipakai di alur ini:
|
||||
- `MISSION_AFTER_FLIGHT_LOCKED`
|
||||
- `FLIGHT_DATA_CREATE_BLOCKED`
|
||||
- `FLIGHT_DATA_AFTER_FLIGHT_LOCKED`
|
||||
- `FLIGHT_DATA_LOCKED`
|
||||
- `FM_REPORT_LOCKED`
|
||||
|
||||
## Catatan Implementasi
|
||||
|
||||
- `after_flight_inspection` tidak lagi memicu refresh `helicopter_usage`
|
||||
- `flight_data` tidak lagi memicu refresh `helicopter_usage`
|
||||
- refresh `helicopter_usage` hanya terjadi saat `fm_report` di-complete
|
||||
- `GET FM Report` tetap aman walau row `helicopter_usage` belum ada
|
||||
|
||||
## Referensi Endpoint
|
||||
|
||||
- `POST /api/v1/mission/create`
|
||||
- `PATCH /api/v1/mission/update/{flight_id}`
|
||||
- `POST /api/v1/flight-data/create`
|
||||
- `PATCH /api/v1/flight-data/update/{id}`
|
||||
- `DELETE /api/v1/flight-data/delete/{id}`
|
||||
- `POST /api/v1/after-flight-inspection/create`
|
||||
- `PATCH /api/v1/after-flight-inspection/update`
|
||||
- `POST /api/v1/fm-reports/complete/{flight_id}`
|
||||
- `GET /api/v1/fm-reports/get/{flight_id}`
|
||||
121
docs/frontend-sso-exchange-flow.md
Normal file
121
docs/frontend-sso-exchange-flow.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Frontend Flow: Microsoft `ssoSilent` + Backend `/auth/exchange`
|
||||
|
||||
This document describes the recommended frontend authentication flow when using:
|
||||
- Microsoft Entra `ssoSilent` on the frontend
|
||||
- Backend session tokens (`access` + `refresh`) issued by `/api/v1/auth/exchange`
|
||||
|
||||
## Goal
|
||||
|
||||
Use Microsoft silent SSO to re-establish identity, then exchange that identity for backend session cookies/tokens used by API requests.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Frontend already integrated with MSAL and can call `ssoSilent`.
|
||||
- Backend has `POST /api/v1/auth/exchange`.
|
||||
- User account is already linked/recognized by backend SSO mapping.
|
||||
- Backend uses cookie-based auth session (`access` + `refresh` cookies).
|
||||
|
||||
## High-Level Sequence
|
||||
|
||||
1. Frontend attempts to call backend API with current backend access session.
|
||||
2. If backend responds `401`, frontend calls `POST /api/v1/auth/refresh` once.
|
||||
3. If refresh fails (`401`), frontend tries Microsoft `ssoSilent`.
|
||||
4. If `ssoSilent` succeeds, frontend calls `POST /api/v1/auth/exchange`.
|
||||
5. Backend issues fresh auth cookies. Frontend retries the original API request.
|
||||
6. If `ssoSilent` fails with `interaction_required`, frontend logs out local app session and redirects user to interactive login.
|
||||
|
||||
## Detailed Frontend Logic
|
||||
|
||||
## 1) App startup / route guard
|
||||
|
||||
1. Try calling a lightweight authenticated endpoint (for example `/api/v1/auth/me`).
|
||||
2. If response is `200`, continue app normally.
|
||||
3. If response is `401`, run the recovery flow below.
|
||||
|
||||
## 2) Recovery flow for backend `401`
|
||||
|
||||
1. Call `POST /api/v1/auth/refresh` once.
|
||||
2. If refresh is successful (`200`), retry the original request and continue.
|
||||
3. If refresh fails (`401`), call Microsoft `ssoSilent`.
|
||||
|
||||
## 3) On successful `ssoSilent`
|
||||
|
||||
The frontend receives a Microsoft auth result (example fields):
|
||||
- `accessToken`
|
||||
- `idToken`
|
||||
- `idTokenClaims`
|
||||
|
||||
Send them to backend exchange endpoint:
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/exchange
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_exchange",
|
||||
"attributes": {
|
||||
"access_token": "<ms_access_token>",
|
||||
"id_token": "<ms_id_token>",
|
||||
"id_token_claims": {
|
||||
"sub": "...",
|
||||
"preferred_username": "user@company.com",
|
||||
"name": "User Name",
|
||||
"tid": "...",
|
||||
"oid": "...",
|
||||
"exp": 1715603600
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- `200`: backend sets fresh session cookies -> frontend retries previous API call.
|
||||
- `401`: identity not linked / invalid token -> treat as not authenticated and move to login.
|
||||
|
||||
## 4) On failed `ssoSilent`
|
||||
|
||||
If frontend receives:
|
||||
- `errorCode = "interaction_required"` (or equivalent no-session/no-cookie condition)
|
||||
|
||||
Then:
|
||||
1. Clear local app auth state (user store, cached flags, pending retries).
|
||||
2. Redirect to interactive login page/flow.
|
||||
|
||||
Do not loop `ssoSilent` repeatedly.
|
||||
|
||||
## Retry Rules (Important)
|
||||
|
||||
- Only attempt each step once per failure chain:
|
||||
- one `/auth/refresh`
|
||||
- one `ssoSilent`
|
||||
- one `/auth/exchange`
|
||||
- Use a request queue/lock to prevent multiple parallel refresh/exchange attempts.
|
||||
- If exchange fails, stop retries and force user to login interactively.
|
||||
|
||||
## Suggested Error Handling Matrix
|
||||
|
||||
- Backend API `401`:
|
||||
- action: `/auth/refresh`
|
||||
- `/auth/refresh` `401`:
|
||||
- action: `ssoSilent`
|
||||
- `ssoSilent` success:
|
||||
- action: `/auth/exchange`
|
||||
- `ssoSilent` `interaction_required`:
|
||||
- action: clear session + redirect login
|
||||
- `/auth/exchange` `401`:
|
||||
- action: clear session + redirect login
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Do not store backend refresh tokens in JavaScript storage.
|
||||
- Prefer `HttpOnly`, `Secure`, `SameSite` cookies from backend.
|
||||
- Avoid logging raw Microsoft tokens in browser console or monitoring payloads.
|
||||
|
||||
## Optional UX Improvement
|
||||
|
||||
When redirecting to login after `interaction_required`, preserve the intended route:
|
||||
- Save current path (`returnTo`) and restore it after successful login.
|
||||
165
docs/mcf-flow.md
Normal file
165
docs/mcf-flow.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Panduan MCF (Maintenance Check Flight): Cara Pakai & Apa yang Terjadi
|
||||
|
||||
> Dokumen ini menjelaskan **cara mengelola MCF** sebuah helikopter — mengaktifkan, mengisi/edit draft, menandatangani (sign), membatalkan tanda tangan, dan membatalkan MCF — beserta **apa yang terjadi** di status helikopter, halaman fleet, dan log.
|
||||
>
|
||||
> Semua endpoint berawalan `/api/v1` dan butuh login. MCF adalah **entity per-helikopter** dengan lifecycle draft → signed.
|
||||
|
||||
---
|
||||
|
||||
## 1. Konsep singkat
|
||||
|
||||
**MCF** = penerbangan uji (check flight) yang wajib dilakukan setelah perawatan tertentu. Selama helikopter "sedang MCF", statusnya `mcf` di fleet.
|
||||
|
||||
Tiga pemain endpoint:
|
||||
|
||||
| Endpoint | Tugasnya |
|
||||
|---|---|
|
||||
| **`set`** | Yes/No + **tempat kerja draft** (buat, isi, edit data check-flight) |
|
||||
| **`sign`** | **finalize** (tandatangani hasil) / **unsign** (balik ke draft) |
|
||||
| **`get` / `get-by-helicopter`** | baca satu MCF / daftar (log, termasuk yang cancelled & signed) |
|
||||
|
||||
> **Tidak ada** endpoint `create`/`delete`/`unassign` lagi — semuanya lewat `set` & `sign`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Status (state) sebuah MCF
|
||||
|
||||
| State | Artinya | `completed_at` | `cancelled_at` | Status heli |
|
||||
|---|---|---|---|---|
|
||||
| **Draft** | Dibuat/diisi, belum ditandatangani | kosong | kosong | `mcf` |
|
||||
| **Signed – passed** | Sudah tanda tangan, hasil **lulus** | terisi | kosong | **clear** (keluar MCF) |
|
||||
| **Signed – failed** | Sudah tanda tangan, hasil **gagal** | terisi | kosong | `mcf` (perlu re-flight) |
|
||||
| **Cancelled** | Draft di-set No ("tidak jadi", tak pernah sign) | kosong | terisi | **lepas** (fleet placeholder) |
|
||||
|
||||
**Aturan penting:**
|
||||
- **1 MCF terbuka (draft) per helikopter.** `set active:true` saat draft sudah ada → **meng-update** draft itu, bukan bikin baru.
|
||||
- **Cancelled hanya untuk draft** (yang belum pernah di-sign). MCF yang sudah signed = **event nyata**, tidak bisa di-cancel.
|
||||
- MCF yang cancelled/signed **tetap tersimpan** di list (`get-by-helicopter`) sebagai **log/history**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Flow lengkap (langkah + apa yang terjadi)
|
||||
|
||||
### Langkah 1 — Aktifkan / isi / edit draft
|
||||
`POST /mcf/set/{helicopter_id}`
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "mcf_set",
|
||||
"attributes": {
|
||||
"active": true,
|
||||
"flight_id": "019d6774-98e8-7fba-9ef4-3795c0da8a13",
|
||||
"af_hours": "1245.3",
|
||||
"landing_count": 3,
|
||||
"date": "2026-06-17",
|
||||
"result": "passed",
|
||||
"notes": "MCF setelah pekerjaan engine mount"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Apa yang terjadi:**
|
||||
- Belum ada draft → **buat draft baru** → **201**.
|
||||
- Sudah ada draft terbuka → **update draft itu** dengan data yang dikirim → **200**.
|
||||
- Semua field data **opsional** — boleh isi sebagian dulu, lengkapi bertahap dengan `set` lagi.
|
||||
- Helikopter masuk **status `mcf`**. Di fleet, object `mcf` muncul (`sign: false`, `af_hours`/`landing_count` pre-fill dari total pesawat kalau belum diisi).
|
||||
|
||||
> `active` boleh diomit → dianggap `true`.
|
||||
|
||||
### Langkah 2 — Finalize (tanda tangan)
|
||||
`POST /mcf/sign/{id}`
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "mcf_sign",
|
||||
"attributes": {
|
||||
"sign": true,
|
||||
"af_hours": "1245.3",
|
||||
"landing_count": 3,
|
||||
"utc_time": "2026-06-17T08:30:00Z",
|
||||
"date": "2026-06-17",
|
||||
"result": "passed"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Apa yang terjadi:**
|
||||
- **Wajib lengkap**: `af_hours`, `landing_count`, `utc_time`, `date`, `result` (else **422**, pointer ke field yang kurang).
|
||||
- MCF jadi **signed/completed** (`completed_at` + `completed_by` terisi).
|
||||
- `result: passed` → **status MCF clear** (helikopter keluar dari MCF).
|
||||
- `result: failed` → helikopter **tetap `mcf`** (perlu re-flight — buat MCF baru lewat `set active:true`).
|
||||
|
||||
### Langkah 3 — Batal tanda tangan (unsign)
|
||||
`POST /mcf/sign/{id}` dengan `{ "sign": false }`
|
||||
|
||||
**Apa yang terjadi:**
|
||||
- MCF yang signed **balik jadi draft** (`completed_at` dikosongkan) — **datanya tetap**.
|
||||
- Bisa di-edit lagi lewat `set`, lalu di-`sign` ulang.
|
||||
|
||||
### Langkah 4 — Nonaktifkan / batal (set No)
|
||||
`POST /mcf/set/{helicopter_id}` dengan `{ "active": false }`
|
||||
|
||||
**Apa yang terjadi:**
|
||||
- Draft (belum sign) → ditandai **`cancelled`** ("tidak jadi") → **status MCF lepas** → fleet balik **placeholder**. Record **tetap di list** sebagai log cancelled.
|
||||
- **Sudah signed → BUKAN batal** (event nyata). MCF dibiarkan; response mengembalikan MCF signed itu apa adanya. Di fleet **datanya tetap muncul** (lihat §5): untuk `passed` helikopter sudah **keluar status MCF** ("active no") tapi data MCF sebelumnya **tetap tampil**; untuk `failed` helikopter tetap `mcf`.
|
||||
- Tidak ada MCF sama sekali → **no-op** (idempotent, sudah "No").
|
||||
|
||||
---
|
||||
|
||||
## 4. Contoh alur khas
|
||||
|
||||
```
|
||||
set {active:true, af_hours, result, ...} → draft dibuat (201)
|
||||
set {active:true, ...revisi...} → draft di-update (200)
|
||||
sign {sign:true, ...lengkap...} → signed (passed=clear / failed=tetap)
|
||||
── kalau salah ──
|
||||
sign {sign:false} → balik draft (data tetap) → edit → sign lagi
|
||||
── kalau tidak jadi ──
|
||||
set {active:false} → cancelled (lepas status, tetap jadi log)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Efek ke Fleet Status
|
||||
|
||||
Object `mcf` di response fleet (`GET /api/v1/fleet-status/...`) selalu mencerminkan **MCF terbaru** helikopter. Field **`active`** = apakah helikopter **sedang dalam MCF** (draft, atau signed-failed yang perlu re-flight); `false` kalau cleared/cancelled/tidak ada.
|
||||
|
||||
| Kondisi MCF terbaru | `active` | Object `mcf` di fleet |
|
||||
|---|---|---|
|
||||
| Tidak ada / cancelled | `false` | **placeholder** — `id: ""`, `sign: false`, `af_hours`/`landing_count` dari total pesawat |
|
||||
| Draft (belum sign) | `true` | id draft, `sign: false`, data pre-fill dari pesawat kalau kosong |
|
||||
| Signed – passed | `false` | id, `sign: true`, snapshot MCF + `signed_by` (data tetap tampil walau status sudah clear) |
|
||||
| Signed – failed | `true` | id, `sign: true`, snapshot + `signed_by` (masih MCF, perlu re-flight) |
|
||||
|
||||
Urutan status di fleet & hold list: **MCF → AOG → Booked → Available** (paling kritikal dulu).
|
||||
|
||||
---
|
||||
|
||||
## 6. Ringkasan endpoint
|
||||
|
||||
| Method | Path | Fungsi |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/v1/mcf/set/{helicopter_id}` | activate/edit draft (`active:true`) · cancel (`active:false`) |
|
||||
| `POST` | `/api/v1/mcf/sign/{id}` | finalize (`sign:true`) · unsign (`sign:false`) |
|
||||
| `GET` | `/api/v1/mcf/get/{id}` | baca satu MCF |
|
||||
| `GET` | `/api/v1/mcf/get-by-helicopter/{helicopter_id}` | daftar MCF helikopter (log) |
|
||||
|
||||
**Permission:** `mcf.create` (untuk `set`), `mcf.complete` (untuk `sign`), `mcf.read` (untuk `get`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Kode error umum
|
||||
|
||||
| Kode | Kapan |
|
||||
|---|---|
|
||||
| `MCF_INVALID_UUID` | `helicopter_id` / `id` bukan UUID valid (422) |
|
||||
| `MCF_INVALID_JSON` | body JSON rusak (400) |
|
||||
| `INVALID_DATA_TYPE` | tipe field salah (mis. `flight_id` dikirim angka) (422) |
|
||||
| `MCF_INVALID_PAYLOAD` | field check-flight kurang/salah saat `sign:true` (422, pointer ke field) |
|
||||
| `MCF_NOT_FOUND` | MCF id tidak ada (404) |
|
||||
|
||||
> **Catatan:** relasi MCF ke penerbangan lewat `flight_id` (opsional). Lihat juga [aog-mcf-nsr-flow.md](aog-mcf-nsr-flow.md) untuk gambaran status helikopter secara keseluruhan (AOG/MCF/NSR).
|
||||
91
docs/password-reset.md
Normal file
91
docs/password-reset.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Password Reset
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /api/v1/auth/forgot-password`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_forgot_password",
|
||||
"attributes": {
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response (`200` always for valid payload):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_forgot_password",
|
||||
"attributes": {
|
||||
"message": "If the account exists, a password reset link has been sent."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/v1/auth/reset-password`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_password",
|
||||
"attributes": {
|
||||
"token": "<raw-token>",
|
||||
"new_password": "StrongPassphrase123!",
|
||||
"confirm_password": "StrongPassphrase123!"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success response (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_password",
|
||||
"attributes": {
|
||||
"message": "Password has been reset successfully. Please log in again."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Invalid/expired/used token response (`400`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "400",
|
||||
"title": "Invalid reset link",
|
||||
"detail": "This reset link is invalid or expired."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `AUTH_PASSWORD_RESET_URL` frontend reset page URL; backend appends `?token=...`
|
||||
- `AUTH_PASSWORD_RESET_TTL` reset token TTL (default `20m`, enforced 15-30m range)
|
||||
- `AUTH_FORGOT_PASSWORD_EMAIL_COOLDOWN` cooldown per email identifier (default `2m`)
|
||||
- `AUTH_FORGOT_PASSWORD_MIN_DURATION` minimum forgot-password response duration (default `150ms`)
|
||||
- `AUTH_FORGOT_PASSWORD_IP_RATE_MAX` per-IP max attempts in window (default `10`)
|
||||
- `AUTH_FORGOT_PASSWORD_IP_RATE_WINDOW` per-IP window duration (default `15m`)
|
||||
|
||||
## Security Decisions
|
||||
|
||||
- Uses `crypto/rand` for reset token entropy.
|
||||
- Stores only SHA-256 token hashes in DB (`password_reset_tokens.token_hash`).
|
||||
- Raw token is only delivered via email link.
|
||||
- Tokens are single-use (`used_at`) and expire (`expires_at`).
|
||||
- New forgot-password request invalidates previous active reset tokens for that user.
|
||||
- Successful reset invalidates all active reset tokens for the user.
|
||||
- Successful reset revokes active sessions by incrementing the MySQL-backed user session version used in JWT claims.
|
||||
- Forgot-password response is generic to prevent account enumeration.
|
||||
- Forgot-password throttling is applied per IP and per email identifier.
|
||||
157
docs/queue-sqs-migration.md
Normal file
157
docs/queue-sqs-migration.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Queue Migration Notes
|
||||
|
||||
This project no longer supports Redis as a runtime dependency. Queue publishing and consumption are SQS-only, while transient auth/runtime state is stored in MySQL.
|
||||
|
||||
## Current Flow
|
||||
|
||||
1. API writes business data to MySQL.
|
||||
2. If `QUEUE_OUTBOX_ENABLED=true`, email jobs are persisted to `email_outbox_messages`.
|
||||
3. Worker polls the outbox and publishes serialized messages to SQS.
|
||||
4. Worker consumes SQS messages with long polling and sends email through Amazon SES v2.
|
||||
5. Successful processing deletes the SQS message.
|
||||
6. Failed processing leaves the message unacked so SQS visibility timeout and DLQ redrive policy handle retries.
|
||||
|
||||
## What Changed
|
||||
|
||||
- Removed Redis producer, consumer, and Redis-backed idempotency paths.
|
||||
- Removed Redis token store used for:
|
||||
- refresh/session bookkeeping
|
||||
- SSO state
|
||||
- TOTP/WebAuthn challenges
|
||||
- security PIN lock/cooldown state
|
||||
- Added MySQL-backed transient stores:
|
||||
- `auth_runtime_tokens`
|
||||
- `queue_idempotency_records`
|
||||
- Simplified bootstrap:
|
||||
- API uses MySQL + optional direct SQS publisher when outbox is disabled.
|
||||
- Worker uses MySQL + SQS only.
|
||||
|
||||
## Required Environment
|
||||
|
||||
Queue and worker settings:
|
||||
|
||||
```bash
|
||||
EMAIL_PROVIDER=mock
|
||||
EMAIL_MOCK_DIR=.local/emails
|
||||
|
||||
QUEUE_MESSAGE_SCHEMA_VERSION=v2
|
||||
QUEUE_ENABLE_LEGACY_PAYLOAD_FALLBACK=true
|
||||
QUEUE_IDEMPOTENCY_KEY_PREFIX=queue:idempotency:email:
|
||||
QUEUE_IDEMPOTENCY_TTL=24h
|
||||
QUEUE_PROCESSING_TTL=15m
|
||||
QUEUE_WORKER_CONCURRENCY=4
|
||||
QUEUE_WORKER_MAX_BATCH_SIZE=10
|
||||
QUEUE_WORKER_PROCESS_TIMEOUT=30s
|
||||
QUEUE_WORKER_SHUTDOWN_TIMEOUT=30s
|
||||
QUEUE_WORKER_BACKOFF_MIN=1s
|
||||
QUEUE_WORKER_BACKOFF_MAX=5m
|
||||
QUEUE_WORKER_PERMANENT_FAILURE_DELAY=30s
|
||||
QUEUE_WORKER_MONITOR_ADDR=127.0.0.1:9090
|
||||
QUEUE_WORKER_HEALTH_ERROR_THRESHOLD=3
|
||||
QUEUE_WORKER_DEPTH_POLL_INTERVAL=30s
|
||||
QUEUE_OUTBOX_ENABLED=true
|
||||
QUEUE_OUTBOX_POLL_INTERVAL=1s
|
||||
QUEUE_OUTBOX_BATCH_SIZE=10
|
||||
QUEUE_OUTBOX_DISPATCH_TIMEOUT=10s
|
||||
QUEUE_OUTBOX_LOCK_TTL=1m
|
||||
QUEUE_OUTBOX_MAX_ATTEMPTS=20
|
||||
```
|
||||
|
||||
SES settings:
|
||||
|
||||
```bash
|
||||
AWS_REGION=ap-southeast-1
|
||||
SES_REGION=
|
||||
SES_FROM=no-reply@example.com
|
||||
SES_CONFIGURATION_SET=
|
||||
SES_SEND_TIMEOUT=10s
|
||||
SES_ENDPOINT=
|
||||
SES_ALLOW_INSECURE_ENDPOINT=false
|
||||
```
|
||||
|
||||
Local/dev recommendation when you do not have AWS SES:
|
||||
|
||||
- set `EMAIL_PROVIDER=mock`
|
||||
- keep SQS on LocalStack if needed
|
||||
- inspect delivered mock emails in `EMAIL_MOCK_DIR`
|
||||
|
||||
AWS / SQS settings:
|
||||
|
||||
```bash
|
||||
AWS_REGION=ap-southeast-1
|
||||
SQS_ENDPOINT=
|
||||
SQS_ALLOW_INSECURE_ENDPOINT=false
|
||||
SQS_QUEUE_URL=https://sqs.ap-southeast-1.amazonaws.com/123456789012/wucher-email
|
||||
SQS_QUEUE_TYPE=standard
|
||||
SQS_MESSAGE_GROUP_ID=email
|
||||
SQS_MAX_MESSAGES=10
|
||||
SQS_WAIT_TIME_SECONDS=20
|
||||
SQS_VISIBILITY_TIMEOUT=60s
|
||||
```
|
||||
|
||||
## SQS Expectations
|
||||
|
||||
Configure the queue outside the application with:
|
||||
|
||||
- long polling enabled
|
||||
- sensible visibility timeout
|
||||
- DLQ redrive policy
|
||||
- `maxReceiveCount` aligned with retry tolerance
|
||||
|
||||
Recommended baseline:
|
||||
|
||||
- `ReceiveMessageWaitTimeSeconds=20`
|
||||
- `VisibilityTimeout=60`
|
||||
- `maxReceiveCount=5`
|
||||
|
||||
## IAM
|
||||
|
||||
API needs `sqs:SendMessage` only when `QUEUE_OUTBOX_ENABLED=false`.
|
||||
|
||||
Worker needs:
|
||||
|
||||
- `sqs:SendMessage` to the main queue when dispatching outbox messages
|
||||
- `sqs:ReceiveMessage`
|
||||
- `sqs:DeleteMessageBatch`
|
||||
- `sqs:ChangeMessageVisibility`
|
||||
- `sqs:GetQueueAttributes`
|
||||
- `ses:SendEmail`
|
||||
|
||||
If SES event tracking is enabled, provision it separately with:
|
||||
|
||||
- SES configuration set event destinations
|
||||
- SNS topic publish permissions
|
||||
- a dedicated SQS event queue and consumer policy
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- Remove these Redis env vars from deployment:
|
||||
- `REDIS_ADDR`
|
||||
- `REDIS_PASSWORD`
|
||||
- `REDIS_DB`
|
||||
- `QUEUE_PRIMARY_BACKEND`
|
||||
- `QUEUE_SECONDARY_BACKEND`
|
||||
- `QUEUE_SECONDARY_REQUIRED`
|
||||
- `QUEUE_CONSUMER_BACKEND`
|
||||
- `QUEUE_IDEMPOTENCY_BACKEND`
|
||||
- `QUEUE_PRODUCER_MODE`
|
||||
- `QUEUE_REDIS_DLQ_KEY`
|
||||
- `AUTH_EMAIL_QUEUE_KEY`
|
||||
- `SQS_DLQ_URL`
|
||||
- Worker now requires MySQL even when it is only consuming SQS, because idempotency state is persisted in MySQL.
|
||||
- Old Redis backlog is not consumed by this version. Drain or discard legacy Redis queues before deploy.
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
1. Ensure MySQL migration/AutoMigrate runs and creates `auth_runtime_tokens` and `queue_idempotency_records`.
|
||||
2. Create or verify the SQS queue and DLQ redrive policy.
|
||||
3. Verify the SES identity/domain and optional configuration set.
|
||||
4. Remove Redis and SMTP config from runtime manifests and secrets.
|
||||
5. Deploy API and worker.
|
||||
6. Verify:
|
||||
- worker `/health`
|
||||
- worker `/metrics`
|
||||
- SQS queue depth
|
||||
- outbox rows are progressing from `pending` to `published`
|
||||
- SES send acceptance/message IDs in worker logs
|
||||
7. Manually delete Redis infrastructure after confirming there is no remaining backlog you still need.
|
||||
349
docs/security-pin.md
Normal file
349
docs/security-pin.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Security PIN Flow
|
||||
|
||||
This document explains how to configure and use a per-user 6-digit Security PIN for sensitive actions.
|
||||
All API payloads follow **JSON:API**.
|
||||
|
||||
## 1) Setup Security PIN (first time)
|
||||
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/setup`
|
||||
|
||||
Auth:
|
||||
- Requires logged-in session (`AUTH` cookie / access token cookie)
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_setup",
|
||||
"attributes": {
|
||||
"pin": "123456",
|
||||
"confirm_pin": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_setup",
|
||||
"attributes": {
|
||||
"configured": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If PIN already exists (`409`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "409",
|
||||
"title": "PIN already configured",
|
||||
"detail": "security PIN is already set"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2) Verify PIN before sensitive action
|
||||
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/verify`
|
||||
|
||||
Auth:
|
||||
- Requires logged-in session
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_verify",
|
||||
"attributes": {
|
||||
"pin": "123456",
|
||||
"action": "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_verify",
|
||||
"attributes": {
|
||||
"verified": true,
|
||||
"action_token": "<one-time-token>",
|
||||
"action_token_ttl_ms": 300000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the returned token on the sensitive API call:
|
||||
- Header: `X-PIN-Verification-Token: <one-time-token>`
|
||||
|
||||
Example sensitive request:
|
||||
```http
|
||||
DELETE /api/v1/users/delete/{uuid}
|
||||
X-PIN-Verification-Token: <one-time-token>
|
||||
Cookie: wucher_at=...
|
||||
```
|
||||
|
||||
Important:
|
||||
- Token is action-bound (`a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74` only for that action).
|
||||
- Token is one-time use.
|
||||
- Token expires by `AUTH_SECURITY_PIN_ACTION_TOKEN_TTL`.
|
||||
- Maximum failed attempts follow `AUTH_SECURITY_PIN_MAX_ATTEMPTS` (default `5`).
|
||||
- When max attempts are reached, API returns `429` and PIN becomes blocked.
|
||||
- Block is permanent until PIN reset succeeds (`AUTH_SECURITY_PIN_LOCK_DURATION=0` means no auto-unlock timeout).
|
||||
|
||||
## 3) Change Security PIN
|
||||
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/change`
|
||||
|
||||
Auth:
|
||||
- Requires logged-in session
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_change",
|
||||
"attributes": {
|
||||
"current_pin": "123456",
|
||||
"new_pin": "654321",
|
||||
"confirm_pin": "654321"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_change",
|
||||
"attributes": {
|
||||
"updated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4) Reset Security PIN Flow
|
||||
|
||||
### 4.1 Request reset link (blocked PIN button)
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/request-reset`
|
||||
|
||||
Auth:
|
||||
- Requires logged-in session
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_request_reset",
|
||||
"attributes": {
|
||||
"password": "<current-account-password>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_pin_request_reset",
|
||||
"attributes": {
|
||||
"message": "If allowed, a security PIN reset link has been sent to your email."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If PIN not blocked (`409`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "409",
|
||||
"title": "PIN not blocked",
|
||||
"detail": "security PIN is not blocked"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Invalid password (`401`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "401",
|
||||
"title": "Unauthorized",
|
||||
"detail": "invalid password"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rate limited (`429`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "429",
|
||||
"title": "Too many requests",
|
||||
"detail": "please wait before requesting another reset link"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Request reset link (public forgot endpoint)
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/forgot`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_forgot_pin",
|
||||
"attributes": {
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response is always generic (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_forgot_pin",
|
||||
"attributes": {
|
||||
"message": "If the account exists, a security PIN reset link has been sent."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Reset PIN with password or Microsoft re-auth
|
||||
Endpoint:
|
||||
`POST /api/v1/auth/pin/reset`
|
||||
|
||||
Notes:
|
||||
- `method` is optional. Default is `password`.
|
||||
- For `method=password`, `token` is required and must be the raw single-use reset token from the email reset link, then send `password`.
|
||||
- For `method=microsoft`, `token` is not required. Send `reauth_token` from Microsoft re-auth directly.
|
||||
|
||||
Request with password re-auth:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_pin",
|
||||
"attributes": {
|
||||
"token": "<raw-token-from-email-link>",
|
||||
"password": "<current-account-password>",
|
||||
"new_pin": "123456",
|
||||
"confirm_pin": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Request with Microsoft re-auth:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_pin",
|
||||
"attributes": {
|
||||
"method": "microsoft",
|
||||
"reauth_token": "<reauth-token-from-/auth/microsoft/reauth?purpose=pin_reset_request>",
|
||||
"new_pin": "123456",
|
||||
"confirm_pin": "123456"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success (`200`):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "auth_reset_pin",
|
||||
"attributes": {
|
||||
"message": "Security PIN has been reset successfully.",
|
||||
"reset_success": true,
|
||||
"redirect_url": "https://app.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Invalid/expired token (`400`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "400",
|
||||
"title": "Invalid reset link",
|
||||
"detail": "This reset link is invalid or expired."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Invalid password (`401`):
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "401",
|
||||
"title": "Unauthorized",
|
||||
"detail": "invalid password"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5) Sensitive endpoints currently protected by PIN token
|
||||
|
||||
The following routes require `X-PIN-Verification-Token`:
|
||||
- `POST /api/v1/auth/invite` (`action=2a59e8b4-5a72-4be3-b8fc-4f49941181c3`)
|
||||
- `POST /api/v1/users/create` (`action=2a59e8b4-5a72-4be3-b8fc-4f49941181c3`)
|
||||
- `PATCH /api/v1/users/update/:uuid` (`action=428fdf48-3f99-46ea-bc47-8fb2e312be08`)
|
||||
- `DELETE /api/v1/users/delete/:uuid` (`action=a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74`)
|
||||
- `POST /api/v1/roles/create` (`action=7fb1d693-8692-4e8f-be86-fd0e29e96bc3`)
|
||||
- `PATCH /api/v1/roles/update/:uuid` (`action=18ca5bbf-ce9d-4e24-8ace-793de56c1de0`)
|
||||
- `DELETE /api/v1/roles/delete/:uuid` (`action=b87fca76-783d-45e7-9022-b2d9c95f6da7`)
|
||||
- `POST /api/v1/roles/:uuid/permissions` (`action=48e523f8-d86f-4b24-b0e3-e213e4d33db4`)
|
||||
- `DELETE /api/v1/roles/:uuid/permissions/:permission_uuid` (`action=f2bf59f8-0783-40fc-94ce-a79e40f84c44`)
|
||||
- `POST /api/v1/roles/:uuid/permissions/assign-bulk` (`action=48e523f8-d86f-4b24-b0e3-e213e4d33db4`)
|
||||
- `POST /api/v1/roles/:uuid/permissions/remove-bulk` (`action=f2bf59f8-0783-40fc-94ce-a79e40f84c44`)
|
||||
|
||||
## 6) Security controls
|
||||
|
||||
- PIN must be exactly 6 digits.
|
||||
- PIN is encrypted at rest (`users.security_pin_encrypted`), not stored in plaintext.
|
||||
- Forgot/reset tokens are random, hashed, single-use, and expiring.
|
||||
- Verify attempts are rate-limited per user with lockout (`AUTH_SECURITY_PIN_MAX_ATTEMPTS`, `AUTH_SECURITY_PIN_LOCK_DURATION`).
|
||||
- Forgot endpoint response is generic to reduce account enumeration risk.
|
||||
|
||||
## 7) Environment variables
|
||||
|
||||
- `AUTH_SECURITY_PIN_RESET_URL`
|
||||
- `AUTH_SECURITY_PIN_RESET_TTL`
|
||||
- `AUTH_FORGOT_SECURITY_PIN_EMAIL_COOLDOWN`
|
||||
- `AUTH_FORGOT_SECURITY_PIN_MIN_DURATION`
|
||||
- `AUTH_SECURITY_PIN_MAX_ATTEMPTS`
|
||||
- `AUTH_SECURITY_PIN_LOCK_DURATION`
|
||||
- `AUTH_SECURITY_PIN_ACTION_TOKEN_TTL`
|
||||
38365
docs/swagger.json
Normal file
38365
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
26515
docs/swagger.yaml
Normal file
26515
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
483
docs/takeover-file-api.md
Normal file
483
docs/takeover-file-api.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Takeover File API
|
||||
|
||||
Dokumen ini menjelaskan alur lengkap file pada `takeover`:
|
||||
|
||||
1. upload intent file
|
||||
2. finalize upload menjadi file manager file tanpa parent takeover
|
||||
3. create takeover baru sambil menempelkan file pakai `file_id`
|
||||
4. update takeover dengan mode add, remove, atau replace file
|
||||
5. response takeover dan FM report yang menampilkan file
|
||||
6. SSE yang tetap dipakai dari file manager
|
||||
|
||||
## Ringkasan Konsep
|
||||
|
||||
Ada 3 objek yang perlu dibedakan:
|
||||
|
||||
1. `file`
|
||||
- file fisik di file manager
|
||||
2. `attachment`
|
||||
- relasi file ke reference bisnis, misalnya `takeover`
|
||||
3. `takeover_file`
|
||||
- relasi khusus takeover ke `file_attachment_id`
|
||||
|
||||
Flow yang dipakai di sistem ini:
|
||||
|
||||
1. FE minta upload intent ke file manager
|
||||
2. FE upload binary file ke storage
|
||||
3. FE finalize file lewat endpoint takeover file dan menerima `file_id`
|
||||
4. FE kirim `file_id` saat create/update takeover
|
||||
5. Backend membuat attachment `ref_type = takeover`
|
||||
6. Backend menyimpan relasi takeover file
|
||||
|
||||
## Endpoint Yang Dipakai
|
||||
|
||||
### File Manager
|
||||
|
||||
#### Request upload intent untuk takeover
|
||||
|
||||
`POST /api/v1/file-manager/files/upload-takeover-file`
|
||||
|
||||
Fungsi:
|
||||
|
||||
1. minta upload intent
|
||||
2. dapat `upload_intent_uuid`
|
||||
3. dapat `upload_url`
|
||||
|
||||
#### Finalize file takeover
|
||||
|
||||
`POST /api/v1/file-manager/files/create-takeover-file`
|
||||
|
||||
Fungsi:
|
||||
|
||||
1. finalize file dari upload intent
|
||||
2. membuat file manager file node
|
||||
3. mengembalikan `file_id` untuk dipakai di takeover create/update
|
||||
|
||||
Endpoint ini tidak membuat `attachment` dan tidak membuat row `takeover_files`.
|
||||
|
||||
Catatan:
|
||||
- `takeover_uuid` tidak wajib
|
||||
- file boleh sementara tanpa parent takeover
|
||||
- attachment takeover baru dibuat saat `POST /api/v1/takeovers/create` atau `PATCH /api/v1/takeovers/update/{id}`
|
||||
|
||||
### Takeover
|
||||
|
||||
#### Create takeover baru
|
||||
|
||||
`POST /api/v1/takeovers/create`
|
||||
|
||||
#### Update takeover
|
||||
|
||||
`PATCH /api/v1/takeovers/update/{id}`
|
||||
|
||||
## Flow Create Takeover Dengan File
|
||||
|
||||
### 1. FE minta upload intent file
|
||||
|
||||
Pakai endpoint:
|
||||
|
||||
`POST /api/v1/file-manager/files/upload-takeover-file`
|
||||
|
||||
Contoh payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_upload_intent",
|
||||
"attributes": {
|
||||
"name": "takeover-report.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"size_bytes": 123456
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Response akan mengembalikan:
|
||||
|
||||
1. `upload_intent_uuid`
|
||||
2. `upload_url`
|
||||
3. `method`
|
||||
4. `required_headers`
|
||||
|
||||
### 2. FE upload binary ke storage
|
||||
|
||||
Gunakan `upload_url` dan `method` dari response upload intent.
|
||||
|
||||
### 3. FE finalize upload menjadi file manager file
|
||||
|
||||
Pakai endpoint:
|
||||
|
||||
`POST /api/v1/file-manager/files/create-takeover-file`
|
||||
|
||||
Contoh payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "file_manager_file_create",
|
||||
"attributes": {
|
||||
"upload_intent_uuid": "019e8713-aa19-7fb7-a510-a0332dc69697",
|
||||
"name": "takeover-report.pdf"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Response akan mengembalikan file manager resource. Ambil `data[0].resource.id` sebagai `file_id`.
|
||||
|
||||
Contoh potongan response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"index": 0,
|
||||
"success": true,
|
||||
"resource": {
|
||||
"type": "file_manager_file",
|
||||
"id": "019e8714-1111-7aaa-8bbb-222222222222",
|
||||
"attributes": {
|
||||
"name": "takeover-report.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. FE kirim takeover create
|
||||
|
||||
Pakai endpoint:
|
||||
|
||||
`POST /api/v1/takeovers/create`
|
||||
|
||||
Field file yang didukung:
|
||||
|
||||
`data.attributes.files[]`
|
||||
|
||||
Struktur item:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "019e8714-1111-7aaa-8bbb-222222222222"
|
||||
}
|
||||
```
|
||||
|
||||
Contoh payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover_create",
|
||||
"attributes": {
|
||||
"base_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"duty_date": "2026-06-22",
|
||||
"notes": "Takeover baru dengan file",
|
||||
"files": [
|
||||
{
|
||||
"file_id": "019e8714-1111-7aaa-8bbb-222222222222"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Backend attach file ke takeover
|
||||
|
||||
Saat request create takeover diproses, backend akan:
|
||||
|
||||
1. membuat takeover
|
||||
2. membaca `file_id` dari `data.attributes.files[]`
|
||||
3. create attachment dengan `ref_type = takeover`
|
||||
4. menyimpan relasi ke `takeover_files`
|
||||
|
||||
Catatan kompatibilitas:
|
||||
- item lama dengan `upload_intent_uuid` masih didukung sebagai fallback
|
||||
- flow baru yang disarankan adalah kirim `file_id`
|
||||
|
||||
## Flow Update Takeover Dengan File
|
||||
|
||||
Pakai endpoint:
|
||||
|
||||
`PATCH /api/v1/takeovers/update/{id}`
|
||||
|
||||
Ada 3 mode yang disarankan.
|
||||
|
||||
### Mode 1. Add file baru
|
||||
|
||||
Gunakan:
|
||||
|
||||
- `files`
|
||||
- atau `files_add`
|
||||
|
||||
Keduanya menerima item yang sama:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "019e8714-3333-7aaa-8bbb-444444444444"
|
||||
}
|
||||
```
|
||||
|
||||
Contoh:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover_update",
|
||||
"id": "019eed5d-36c2-72d9-acab-388b4e41a949",
|
||||
"attributes": {
|
||||
"notes": "Tambah file baru",
|
||||
"files_add": [
|
||||
{
|
||||
"file_id": "019e8714-3333-7aaa-8bbb-444444444444"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mode 2. Remove file tertentu
|
||||
|
||||
Gunakan:
|
||||
|
||||
`files_remove`
|
||||
|
||||
Isi field ini adalah `file_attachment_id`, bukan `takeover_file.id`.
|
||||
|
||||
Contoh:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover_update",
|
||||
"id": "019eed5d-36c2-72d9-acab-388b4e41a949",
|
||||
"attributes": {
|
||||
"files_remove": [
|
||||
"019d7000-aaaa-7bbb-8ccc-222222222222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Saat remove:
|
||||
|
||||
1. relasi `takeover_files` dihapus
|
||||
2. attachment file-manager juga dihapus
|
||||
3. file lifecycle ikut direconcile oleh file manager
|
||||
|
||||
### Mode 3. Replace semua file
|
||||
|
||||
Gunakan:
|
||||
|
||||
`files_replace`
|
||||
|
||||
Mode ini:
|
||||
|
||||
1. menghapus semua file lama dari takeover
|
||||
2. menambahkan daftar file baru
|
||||
|
||||
Aturan:
|
||||
|
||||
1. `files_replace` tidak boleh digabung dengan `files`
|
||||
2. `files_replace` tidak boleh digabung dengan `files_add`
|
||||
3. `files_replace` tidak boleh digabung dengan `files_remove`
|
||||
|
||||
Contoh:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover_update",
|
||||
"id": "019eed5d-36c2-72d9-acab-388b4e41a949",
|
||||
"attributes": {
|
||||
"files_replace": [
|
||||
{
|
||||
"file_id": "019e8714-5555-7aaa-8bbb-666666666666"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mode campuran: add + remove
|
||||
|
||||
Kalau kamu ingin menambah file baru sambil menghapus file lama, pakai:
|
||||
|
||||
1. `files_add`
|
||||
2. `files_remove`
|
||||
|
||||
Contoh:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover_update",
|
||||
"id": "019eed5d-36c2-72d9-acab-388b4e41a949",
|
||||
"attributes": {
|
||||
"files_add": [
|
||||
{
|
||||
"file_id": "019e8714-7777-7aaa-8bbb-888888888888"
|
||||
}
|
||||
],
|
||||
"files_remove": [
|
||||
"019d7000-aaaa-7bbb-8ccc-222222222222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Create takeover
|
||||
|
||||
Field yang wajib:
|
||||
|
||||
1. `duty_date`
|
||||
|
||||
Field file:
|
||||
|
||||
1. `files[]` opsional
|
||||
2. untuk flow baru, setiap item file berisi:
|
||||
- `file_id`
|
||||
|
||||
Catatan:
|
||||
- `upload_intent_uuid` + `name` masih bisa dipakai sebagai fallback lama
|
||||
- untuk flow baru, gunakan `file_id` dari response `/create-takeover-file`
|
||||
|
||||
### Update takeover
|
||||
|
||||
Field file yang tersedia:
|
||||
|
||||
1. `files`
|
||||
2. `files_add`
|
||||
3. `files_remove`
|
||||
4. `files_replace`
|
||||
|
||||
Aturan kombinasi:
|
||||
|
||||
1. `files_replace` eksklusif
|
||||
2. `files` dan `files_add` diperlakukan sebagai add file baru
|
||||
3. `files_remove` harus berisi UUID attachment yang masih terhubung ke takeover
|
||||
|
||||
## Response Takeover
|
||||
|
||||
Response takeover sekarang punya field:
|
||||
|
||||
`data.attributes.files`
|
||||
|
||||
Contoh item:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "takeover_file",
|
||||
"id": "019d7000-aaaa-7bbb-8ccc-111111111111",
|
||||
"attributes": {
|
||||
"file_attachment_id": "019d7000-aaaa-7bbb-8ccc-222222222222",
|
||||
"file_name": "takeover-report.pdf",
|
||||
"download_url": "https://..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Arti field:
|
||||
|
||||
1. `id`
|
||||
- UUID relasi `takeover_file`
|
||||
2. `file_attachment_id`
|
||||
- UUID attachment file-manager
|
||||
3. `file_name`
|
||||
- nama file yang tersimpan
|
||||
4. `download_url`
|
||||
- URL download file jika storage tersedia
|
||||
|
||||
## Response FM Report
|
||||
|
||||
Response `fm report` juga menampilkan file takeover melalui:
|
||||
|
||||
`data.attributes.takeover_files`
|
||||
|
||||
Isi resource sama seperti response takeover:
|
||||
|
||||
1. `id`
|
||||
2. `file_attachment_id`
|
||||
3. `file_name`
|
||||
4. `download_url`
|
||||
|
||||
## SSE
|
||||
|
||||
Flow takeover file tetap memakai SSE file manager yang sudah ada.
|
||||
|
||||
Event utama:
|
||||
|
||||
`file.updated`
|
||||
|
||||
Artinya saat file berhasil difinalisasi atau status file berubah, FE tetap bisa mendengarkan event realtime dari stream file manager.
|
||||
|
||||
Endpoint stream:
|
||||
|
||||
`GET /api/v1/file-manager/events`
|
||||
|
||||
Catatan:
|
||||
|
||||
1. belum ada stream SSE takeover-file khusus
|
||||
2. yang dipakai adalah stream file manager yang sudah eksisting
|
||||
|
||||
## Migration dan Struktur Database
|
||||
|
||||
Tabel baru:
|
||||
|
||||
`takeover_files`
|
||||
|
||||
Kolom:
|
||||
|
||||
1. `id`
|
||||
2. `takeover_id`
|
||||
3. `file_attachment_id`
|
||||
|
||||
Migration:
|
||||
|
||||
`migrations/20260622_create_takeover_files.sql`
|
||||
|
||||
## Best Practice Pemakaian
|
||||
|
||||
1. Finalize upload dulu lewat `/api/v1/file-manager/files/create-takeover-file`
|
||||
2. Kalau takeover sudah ada, gunakan `PATCH /api/v1/takeovers/update/{id}`
|
||||
3. Pakai `files_replace` hanya kalau memang ingin mengganti semua file
|
||||
4. Pakai `files_add` dan `files_remove` kalau ingin perubahan parsial yang lebih aman
|
||||
5. Untuk menghapus file, gunakan `file_attachment_id` dari response takeover
|
||||
|
||||
## Contoh Alur Yang Disarankan
|
||||
|
||||
### Create takeover baru dengan 1 file
|
||||
|
||||
1. request upload intent
|
||||
2. upload binary ke storage
|
||||
3. panggil `POST /api/v1/file-manager/files/create-takeover-file`
|
||||
4. ambil `file_id` dari response
|
||||
5. panggil `POST /api/v1/takeovers/create` dengan `files[].file_id`
|
||||
|
||||
### Update takeover dan tambah file baru
|
||||
|
||||
1. request upload intent
|
||||
2. upload binary ke storage
|
||||
3. panggil `POST /api/v1/file-manager/files/create-takeover-file`
|
||||
4. ambil `file_id` dari response
|
||||
5. panggil `PATCH /api/v1/takeovers/update/{id}` dengan `files_add[].file_id`
|
||||
|
||||
### Update takeover dan replace semua file
|
||||
|
||||
1. request upload intent baru untuk file pengganti
|
||||
2. upload binary ke storage
|
||||
3. panggil `POST /api/v1/file-manager/files/create-takeover-file`
|
||||
4. ambil `file_id` dari response
|
||||
5. panggil `PATCH /api/v1/takeovers/update/{id}` dengan `files_replace[].file_id`
|
||||
578
docs/takeover-step-by-step.md
Normal file
578
docs/takeover-step-by-step.md
Normal file
@@ -0,0 +1,578 @@
|
||||
# Takeover Flow Step by Step
|
||||
|
||||
Dokumen ini menjelaskan flow `takeover` dari sisi frontend secara detail: endpoint yang dipakai, urutan request, payload yang wajib dan opsional, serta contoh request/response.
|
||||
|
||||
Untuk detail khusus file takeover, lihat [docs/takeover-file-api.md](./takeover-file-api.md).
|
||||
|
||||
## Ringkasan Singkat
|
||||
|
||||
`Takeover` adalah endpoint atomic untuk membuat satu paket data sekaligus:
|
||||
|
||||
1. `takeover_ac`
|
||||
2. `duty_roster`
|
||||
3. `reserve_ac`
|
||||
4. `flight_inspection`
|
||||
5. `before_flight_inspection`
|
||||
6. `flight_prep_check`
|
||||
7. `flight`
|
||||
|
||||
Artinya frontend cukup kirim **1 request create takeover**, backend akan membuat seluruh record terkait dalam satu transaksi.
|
||||
|
||||
## Endpoint Yang Tersedia
|
||||
|
||||
Ada 1 route create takeover yang menuju handler yang sama:
|
||||
|
||||
1. `POST /api/v1/takeovers/create`
|
||||
|
||||
Keduanya memakai permission:
|
||||
|
||||
`reserve_ac.create`
|
||||
|
||||
Route lain yang tersedia:
|
||||
|
||||
1. `GET /api/v1/takeovers/get-all`
|
||||
2. `GET /api/v1/takeovers/get/:id`
|
||||
3. `PATCH /api/v1/takeovers/update/:id`
|
||||
4. `DELETE /api/v1/takeovers/delete/:id`
|
||||
|
||||
Untuk dokumentasi ini fokus utama ada pada flow `create takeover`.
|
||||
|
||||
## Step by Step Flow
|
||||
|
||||
### Step 1. Siapkan data master terlebih dahulu
|
||||
|
||||
Sebelum hit create takeover, frontend harus sudah punya:
|
||||
|
||||
1. `base_id`
|
||||
2. `helicopter_id`
|
||||
3. `duty_date`
|
||||
4. daftar crew untuk roster
|
||||
5. data inspeksi sebelum terbang
|
||||
6. data persiapan flight
|
||||
|
||||
Kalau salah satu ID tidak valid atau tidak ditemukan, backend akan gagal.
|
||||
|
||||
### Step 2. Susun payload utama
|
||||
|
||||
Payload mengikuti format JSON:API sederhana:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover",
|
||||
"attributes": {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Field `type` menerima:
|
||||
|
||||
1. `takeover`
|
||||
2. `takeover_create`
|
||||
|
||||
Frontend boleh kirim salah satu, tapi disarankan konsisten pakai `takeover_create` atau `takeover` sesuai standar proyek yang dipakai tim.
|
||||
|
||||
### Step 3. Kirim request create
|
||||
|
||||
Request create akan:
|
||||
|
||||
1. validasi JSON
|
||||
2. validasi struktur payload
|
||||
3. validasi UUID `base_id`
|
||||
4. validasi UUID `helicopter_id`
|
||||
5. validasi tanggal `duty_date`
|
||||
6. membangun roster crew
|
||||
7. membuat seluruh data dalam 1 transaksi
|
||||
8. mengembalikan response takeover lengkap
|
||||
|
||||
Kalau satu sub-step gagal, seluruh transaksi gagal.
|
||||
|
||||
### Step 4. Terima response
|
||||
|
||||
Response sukses akan mengembalikan struktur `takeover` lengkap berisi:
|
||||
|
||||
1. `takeover_ac_id`
|
||||
2. `duty_roster_id`
|
||||
3. `flight_id`
|
||||
4. `reserve_ac_id`
|
||||
5. `flight_inspection_id`
|
||||
6. `shift_start`
|
||||
7. `shift_end`
|
||||
8. `base`
|
||||
9. `helicopter`
|
||||
10. `roster_detail`
|
||||
11. `inspection`
|
||||
12. `flight`
|
||||
13. `duty_roster`
|
||||
|
||||
Frontend sebaiknya memakai response ini sebagai sumber data final, bukan mengasumsikan data lokal sama persis dengan hasil backend.
|
||||
|
||||
## Request Detail
|
||||
|
||||
### Endpoint
|
||||
|
||||
`POST /api/v1/takeovers/create`
|
||||
|
||||
atau
|
||||
|
||||
`POST /api/v1/reserve-acs/takeovers/create`
|
||||
|
||||
### Headers
|
||||
|
||||
Gunakan header umum API proyek:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover",
|
||||
"attributes": {
|
||||
"base_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"duty_date": "2026-04-15",
|
||||
"notes": "Opsional catatan takeover",
|
||||
"shift_time": {
|
||||
"shift_start": "06:00",
|
||||
"shift_end": "18:00"
|
||||
},
|
||||
"roster_detail": {
|
||||
"pilot": [
|
||||
{
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d",
|
||||
"crew_type": "main",
|
||||
"flight_instructor": true,
|
||||
"line_checker": false,
|
||||
"supervisor": false,
|
||||
"examiner": false,
|
||||
"co_pilot": true,
|
||||
"shift_date": {
|
||||
"date_start": "2026-04-15",
|
||||
"date_end": "2026-04-15"
|
||||
},
|
||||
"shift_time": {
|
||||
"shift_start": "06:00",
|
||||
"shift_end": "14:00"
|
||||
}
|
||||
}
|
||||
],
|
||||
"doctor": [
|
||||
{
|
||||
"id": "018f3a5c-7a6f-7c2a-8d4a-4b5b1f6c8e9d"
|
||||
}
|
||||
],
|
||||
"rescuer": [],
|
||||
"helper": [],
|
||||
"other_person": [
|
||||
{
|
||||
"name": "Guest Person",
|
||||
"mobile_phone": "+43-1234",
|
||||
"email": "guest@example.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"inspection": {
|
||||
"inspection_date": "2026-04-15",
|
||||
"before": {
|
||||
"oil_engine_nr1_checked": true,
|
||||
"oil_engine_nr2_checked": false,
|
||||
"hydraulic_lh_checked": true,
|
||||
"hydraulic_rh_checked": true,
|
||||
"oil_transmission_mgb_checked": true,
|
||||
"oil_transmission_igb_checked": true,
|
||||
"oil_transmission_tgb_checked": true,
|
||||
"fuel_amount": 50.5,
|
||||
"fuel_unit": "LT",
|
||||
"fuel_added_amount": 10,
|
||||
"note": "Opsional note inspeksi sebelum terbang",
|
||||
"file_checklist": [
|
||||
{
|
||||
"helicopter_file_id": "019d7000-aaaa-7bbb-8ccc-111111111111",
|
||||
"is_done": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"prepare": {
|
||||
"notam_briefing": true,
|
||||
"weather_briefing": true,
|
||||
"operational_flight_plan": false,
|
||||
"file_checklist": [
|
||||
{
|
||||
"helicopter_file_id": "019d7000-bbbb-7ccc-8ddd-222222222222",
|
||||
"is_done": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Penjelasan Field
|
||||
|
||||
### `data`
|
||||
|
||||
Container utama request.
|
||||
|
||||
### `data.type`
|
||||
|
||||
Nilai yang diterima:
|
||||
|
||||
1. `takeover`
|
||||
2. `takeover_create`
|
||||
|
||||
### `data.attributes.base_id`
|
||||
|
||||
Wajib. UUID base yang dipakai untuk takeover.
|
||||
|
||||
Backend akan memvalidasi:
|
||||
|
||||
1. format UUID
|
||||
2. base harus ada di database
|
||||
|
||||
### `data.attributes.duty_date`
|
||||
|
||||
Wajib. Format tanggal harus `YYYY-MM-DD`.
|
||||
|
||||
Contoh:
|
||||
|
||||
`2026-04-15`
|
||||
|
||||
### `data.attributes.notes`
|
||||
|
||||
Opsional.
|
||||
|
||||
Gunakan untuk catatan tambahan takeover. Jika tidak ada, field ini boleh tidak dikirim atau dikirim `null`.
|
||||
|
||||
### `data.attributes.shift_time`
|
||||
|
||||
Secara schema tidak diberi `validate:"required"`, tetapi secara proses bisnis ini dianggap bagian penting dari takeover.
|
||||
|
||||
Format:
|
||||
|
||||
```json
|
||||
{
|
||||
"shift_start": "06:00",
|
||||
"shift_end": "18:00"
|
||||
}
|
||||
```
|
||||
|
||||
Keterangan:
|
||||
|
||||
1. `shift_start` adalah waktu mulai tugas.
|
||||
2. `shift_end` adalah waktu selesai tugas.
|
||||
|
||||
Jika frontend ingin aman, selalu kirim kedua field ini.
|
||||
|
||||
### `data.attributes.roster_detail`
|
||||
|
||||
Wajib secara bisnis. Berisi daftar crew yang akan masuk ke `duty_roster`.
|
||||
|
||||
Strukturnya terdiri dari:
|
||||
|
||||
1. `pilot`
|
||||
2. `doctor`
|
||||
3. `rescuer`
|
||||
4. `helper`
|
||||
5. `other_person`
|
||||
|
||||
### `data.attributes.helicopter_id`
|
||||
|
||||
Wajib. UUID helicopter yang dipakai pada takeover.
|
||||
|
||||
Backend akan memvalidasi:
|
||||
|
||||
1. format UUID
|
||||
2. helicopter harus ada di database
|
||||
|
||||
### `data.attributes.inspection`
|
||||
|
||||
Wajib. Ini adalah paket inspeksi takeover.
|
||||
|
||||
#### `inspection.inspection_date`
|
||||
|
||||
Wajib. Format `YYYY-MM-DD`.
|
||||
|
||||
#### `inspection.before`
|
||||
|
||||
Wajib sebagai objek, tetapi isi field di dalamnya sebagian besar opsional.
|
||||
|
||||
Field opsional:
|
||||
|
||||
1. `oil_engine_nr1_checked`
|
||||
2. `oil_engine_nr2_checked`
|
||||
3. `hydraulic_lh_checked`
|
||||
4. `hydraulic_rh_checked`
|
||||
5. `oil_transmission_mgb_checked`
|
||||
6. `oil_transmission_igb_checked`
|
||||
7. `oil_transmission_tgb_checked`
|
||||
8. `fuel_amount`
|
||||
9. `fuel_unit`
|
||||
10. `fuel_added_amount`
|
||||
11. `note`
|
||||
12. `file_checklist`
|
||||
|
||||
#### `inspection.before.fuel_unit`
|
||||
|
||||
Jika dikirim, nilai yang diterima backend adalah:
|
||||
|
||||
1. `LT`
|
||||
2. `KG`
|
||||
3. `POUND`
|
||||
|
||||
Kalau nilainya tidak valid, request akan gagal.
|
||||
|
||||
#### `inspection.before.file_checklist`
|
||||
|
||||
Array opsional.
|
||||
|
||||
Setiap item harus punya:
|
||||
|
||||
1. `helicopter_file_id`
|
||||
2. `is_done`
|
||||
|
||||
#### `inspection.prepare`
|
||||
|
||||
Wajib sebagai objek, tetapi field di dalamnya opsional.
|
||||
|
||||
Field opsional:
|
||||
|
||||
1. `notam_briefing`
|
||||
2. `weather_briefing`
|
||||
3. `operational_flight_plan`
|
||||
4. `file_checklist`
|
||||
|
||||
#### `inspection.prepare.file_checklist`
|
||||
|
||||
Array opsional.
|
||||
|
||||
Setiap item harus punya:
|
||||
|
||||
1. `helicopter_file_id`
|
||||
2. `is_done`
|
||||
|
||||
## Detail Roster
|
||||
|
||||
### `roster_detail.pilot`
|
||||
|
||||
Array pilot. Setiap item dapat berisi:
|
||||
|
||||
1. `id` - UUID crew
|
||||
2. `crew_type` - biasanya `main` atau `additional`
|
||||
3. `flight_instructor` - boolean
|
||||
4. `line_checker` - boolean
|
||||
5. `supervisor` - boolean
|
||||
6. `examiner` - boolean
|
||||
7. `co_pilot` - boolean
|
||||
8. `shift_date` - opsional
|
||||
9. `shift_time` - opsional
|
||||
|
||||
### `roster_detail.doctor`
|
||||
|
||||
Array crew doctor.
|
||||
|
||||
Setiap item:
|
||||
|
||||
1. `id` - UUID crew
|
||||
2. `shift_date` - opsional
|
||||
3. `shift_time` - opsional
|
||||
|
||||
### `roster_detail.rescuer`
|
||||
|
||||
Array crew rescuer.
|
||||
|
||||
Setiap item:
|
||||
|
||||
1. `id` - UUID crew
|
||||
2. `shift_date` - opsional
|
||||
3. `shift_time` - opsional
|
||||
|
||||
### `roster_detail.helper`
|
||||
|
||||
Array crew helper.
|
||||
|
||||
Setiap item:
|
||||
|
||||
1. `id` - UUID crew
|
||||
2. `shift_date` - opsional
|
||||
3. `shift_time` - opsional
|
||||
|
||||
### `roster_detail.other_person`
|
||||
|
||||
Array orang tambahan yang bukan crew terdaftar.
|
||||
|
||||
Setiap item:
|
||||
|
||||
1. `name`
|
||||
2. `mobile_phone`
|
||||
3. `email`
|
||||
|
||||
## Field Optional Yang Perlu Diperhatikan Frontend
|
||||
|
||||
Ini yang paling penting supaya frontend tidak bingung:
|
||||
|
||||
### Optional level 1
|
||||
|
||||
1. `notes`
|
||||
|
||||
### Optional level 2 di `inspection.before`
|
||||
|
||||
1. Semua checkbox `*_checked`
|
||||
2. `fuel_amount`
|
||||
3. `fuel_unit`
|
||||
4. `fuel_added_amount`
|
||||
5. `note`
|
||||
6. `file_checklist`
|
||||
|
||||
### Optional level 2 di `inspection.prepare`
|
||||
|
||||
1. `notam_briefing`
|
||||
2. `weather_briefing`
|
||||
3. `operational_flight_plan`
|
||||
4. `file_checklist`
|
||||
|
||||
### Optional level 2 di roster detail
|
||||
|
||||
1. `shift_date`
|
||||
2. `shift_time`
|
||||
|
||||
### Field yang tetap harus dianggap wajib oleh frontend
|
||||
|
||||
Walaupun beberapa field di struct tidak pakai `validate:"required"`, frontend sebaiknya tetap menyiapkan:
|
||||
|
||||
1. `base_id`
|
||||
2. `duty_date`
|
||||
3. `roster_detail`
|
||||
4. `helicopter_id`
|
||||
5. `inspection`
|
||||
6. `inspection.inspection_date`
|
||||
|
||||
Alasannya:
|
||||
|
||||
1. backend memakai data itu untuk membangun transaksi
|
||||
2. frontend akan lebih stabil jika payload selalu lengkap
|
||||
3. respons error dari backend akan lebih mudah dihindari
|
||||
|
||||
## Contoh Request Minimal Yang Aman
|
||||
|
||||
Kalau ingin payload ringkas tetapi masih aman, gunakan pola berikut:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover",
|
||||
"attributes": {
|
||||
"base_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"duty_date": "2026-04-15",
|
||||
"shift_time": {
|
||||
"shift_start": "06:00",
|
||||
"shift_end": "18:00"
|
||||
},
|
||||
"roster_detail": {
|
||||
"pilot": [],
|
||||
"doctor": [],
|
||||
"rescuer": [],
|
||||
"helper": [],
|
||||
"other_person": []
|
||||
},
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"inspection": {
|
||||
"inspection_date": "2026-04-15",
|
||||
"before": {},
|
||||
"prepare": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Catatan:
|
||||
|
||||
1. Ini minimal secara struktur.
|
||||
2. Secara bisnis, frontend biasanya tetap perlu mengisi roster dan checklist sesuai kebutuhan operasional.
|
||||
|
||||
## Response Sukses
|
||||
|
||||
HTTP status:
|
||||
|
||||
`201 Created`
|
||||
|
||||
Contoh bentuk response:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"type": "takeover",
|
||||
"attributes": {
|
||||
"takeover_ac_id": "019d8000-aaaa-7bbb-8ccc-333333333333",
|
||||
"duty_roster_id": "019d8000-bbbb-7ccc-8ddd-444444444444",
|
||||
"flight_id": "019d8000-cccc-7ddd-8eee-555555555555",
|
||||
"reserve_ac_id": "019d8000-dddd-7eee-8fff-666666666666",
|
||||
"flight_inspection_id": "019d8000-eeee-7fff-9000-777777777777",
|
||||
"shift_start": "06:00",
|
||||
"shift_end": "18:00",
|
||||
"base_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"duty_date": "2026-04-15",
|
||||
"helicopter_id": "019d6771-5fb5-7337-837f-bc5ed85181a1",
|
||||
"notes": "Opsional catatan takeover",
|
||||
"roster_detail": {
|
||||
"pilot": [],
|
||||
"doctor": [],
|
||||
"rescuer": [],
|
||||
"helper": [],
|
||||
"other_person": []
|
||||
},
|
||||
"inspection": {},
|
||||
"flight": {},
|
||||
"duty_roster": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Yang Umum
|
||||
|
||||
### 400 Bad Request
|
||||
|
||||
Biasanya terjadi karena:
|
||||
|
||||
1. JSON tidak valid
|
||||
2. create gagal di service
|
||||
3. UUID tidak bisa diproses
|
||||
4. data referensi tidak ditemukan
|
||||
|
||||
### 422 Unprocessable Entity
|
||||
|
||||
Biasanya terjadi karena:
|
||||
|
||||
1. `type` salah
|
||||
2. field wajib tidak ada
|
||||
3. format UUID salah
|
||||
4. tanggal bukan format `YYYY-MM-DD`
|
||||
5. `fuel_unit` tidak valid
|
||||
6. roster detail tidak valid
|
||||
|
||||
## Saran Implementasi Frontend
|
||||
|
||||
1. Selalu kirim payload lengkap dari form state.
|
||||
2. Jangan kirim field boolean sebagai string.
|
||||
3. Pastikan UUID sudah tervalidasi sebelum submit.
|
||||
4. Validasi tanggal di frontend dengan format `YYYY-MM-DD`.
|
||||
5. Jika user tidak isi field opsional, lebih baik field itu diomit daripada dikirim string kosong.
|
||||
6. Setelah request sukses, gunakan response backend untuk refresh state halaman.
|
||||
|
||||
## Catatan Penting
|
||||
|
||||
1. `takeover` ini bersifat atomic.
|
||||
2. Jika satu bagian gagal, semua data batal dibuat.
|
||||
3. `notes` adalah satu-satunya field tingkat atas yang jelas opsional.
|
||||
4. Banyak field di nested object yang secara schema terlihat opsional, tetapi secara flow bisnis tetap perlu diisi agar hasilnya masuk akal.
|
||||
Reference in New Issue
Block a user