360 lines
8.9 KiB
Markdown
360 lines
8.9 KiB
Markdown
# 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.
|