Offline-First Architecture โข PHP 8.1+ โข JWT โข AES-256-GCM โข UUID-Based Upload โข SSE
Base URL: https://android.urice.id
Content-Type: application/json (kecuali SSE:
text/event-stream, Upload: multipart/form-data)
{"status": int, "message": string, "data": mixed}date_default_timezone_set('Asia/Jakarta')) dan MySQL
(SET time_zone = '+07:00') menggunakan zona waktu yang sama. Ketidakcocokan akan
menyebabkan conflict detection ngaco karena strtotime() &
NOW() menghasilkan nilai berbeda.
Setiap tabel operasional memiliki 4 kolom wajib yang menangani state offline-first & conflict resolution.
| Column | Type | Trigger Update | Kegunaan Utama |
|---|---|---|---|
createdAt |
TIMESTAMP | Saat record pertama dibuat | Audit trail, sorting berdasarkan waktu pembuatan |
updatedAt |
TIMESTAMP | Setiap insert/update (lokal atau server) | Conflict resolution, parameter sinceTimestamp saat pull |
syncStatus |
ENUM | Proses push/pull & conflict detection | State queue: PENDING โ SYNCED โ CONFLICT โ
FAILED |
lastSyncedAt |
TIMESTAMP | Hanya saat push/pull berhasil dikonfirmasi server | Marker keberhasilan sync, optimasi pull berikutnya |
attendance.photo &
users.photoName menyimpan 1 path relatif. task_evidence.photo menyimpan
JSON array string ["path1.jpg","path2.jpg"].| Tabel | Column | Type | Keterangan |
|---|---|---|---|
projects |
supervisorServerId |
CHAR(36) | FK ke users.userServer. Penanggung jawab proyek |
projects |
location |
VARCHAR(255) | Alamat/lokasi pelaksanaan proyek |
project_participants |
assignmentDate |
DATETIME | Tanggal & waktu pelaksanaan penugasan |
Sistem menggunakan pendekatan Upload-First โ Sync-Second. File media harus sukses terupload ke server sebelum record database dikirim.
updatedAt=NOW(), syncStatus='PENDING',
photoLocalPath='/storage/...'PENDING records โ Upload
foto ke POST /api/upload โ Server return path relatif.photoLocalPath โ server path. Kirim
payload ke POST /api/sync?action=push.updatedAt. Client lebih
baru โ UPDATE โ SYNCED. Server lebih baru โ SKIP โ
CONFLICT โ return UUID ke client.syncStatus &
lastSyncedAt โ hapus file lokal jika SYNCED. Tampilkan UI merge
jika CONFLICT.syncStatus| Status | Pemicu | Aksi Selanjutnya |
|---|---|---|
PENDING |
Data diubah/dibuat saat offline | Akan di-upload & dipush saat jaringan tersedia |
SYNCED |
Push/Pull berhasil dikonfirmasi server | Hapus cache lokal, siap operasi offline baru |
CONFLICT |
Server menolak karena updatedAt lebih lama |
UI menampilkan prompt resolve. Setelah pilih versi โ set PENDING โ push
ulang |
FAILED |
Network error / server 5xx / FK violation | Tetap PENDING, retry otomatis dengan exponential backoff |
Setiap upload disimpan dalam folder unik berdasarkan {table}/{uuid}/. Client wajib
generate UUID v4 di lokal sebelum upload agar server bisa menyimpan file dengan
struktur yang rapi & predictable.
uploads/
โโโ .htaccess (Block PHP & Indexing)
โโโ attendance/
โ โโโ 550e8400-e29b-41d4-a716-446655440011/
โ โโโ 1715500000_a1b2c3d4.jpg โ Single file
โโโ users/
โ โโโ ffef-bce3-0e8a-4c87-80e2/
โ โโโ 1715500100_e5f6g7h8.png โ Profile photo
โโโ task_evidence/
โโโ 550e8400-.../
โโโ img1_1715500200.jpg โ Multiple files
โโโ img2_1715500201.jpg
โโโ vid3_1715500202.mp4
POST /api/upload| Parameter | Type | Required | Desc |
|---|---|---|---|
type |
string | Yes | Nama tabel: attendance, users, task_evidence |
uuid |
string | Yes | UUID v4 dari record lokal (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
photos[] |
file[] | Yes | Array binary files. Support multiple upload sekaligus. Max 5MB/file, ext:
jpg,jpeg,png,webp |
Gunakan multipart/form-data. Perhatikan penamaan field photos[] (dengan
kurung siku) agar PHP mengenali sebagai array.
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="type"
task_evidence
------WebKitFormBoundary
Content-Disposition: form-data; name="uuid"
550e8400-e29b-41d4-a716-446655440099
------WebKitFormBoundary
Content-Disposition: form-data; name="photos[]"; filename="bukti1.jpg"
Content-Type: image/jpeg
(binary data...)
------WebKitFormBoundary
Content-Disposition: form-data; name="photos[]"; filename="bukti2.jpg"
Content-Type: image/jpeg
(binary data...)
------WebKitFormBoundary--
{
"status": 200,
"message": "Upload completed",
"data": {
"files": [
{ "status": "success", "filename": "1715500200_a1b2.jpg", "path": "uploads/task_evidence/550e8400-.../1715500200_a1b2.jpg" },
{ "status": "success", "filename": "1715500201_c3d4.jpg", "path": "uploads/task_evidence/550e8400-.../1715500201_c3d4.jpg" }
]
}
}
attendance.photo: Simpan string path tunggal.task_evidence.photo: Simpan JSON Array dari response.["uploads/task_evidence/.../img1.jpg", "uploads/task_evidence/.../img2.jpg"]PULL, client cukup JSON.parse() kolom tersebut untuk mendapatkan list
foto.
| Strategi | Cara Kerja | Rekomendasi |
|---|---|---|
| โ Proactive | Cek exp di JWT lokal. Jika sisa < 7 hari, otomatis call
/auth/refresh |
WAJIB. Hindari 401 saat user bekerja. |
| ๐ Reactive | Interceptor tangkap 401 โ call /refresh โ retry request awal
|
BACKUP. Handle clock skew atau app lama idle. |
| โ ๏ธ Deactivation | Server balikin 403 saat isActive=false |
Client harus clear session & redirect login. JANGAN retry. |
/auth/verify & /auth/refresh yang mengecek DB isActive adalah
satu-satunya cara aman memutus sesi.๐ฅ Body: {"username":"superadmin","password":"123456"}
๐ค โ
(200):
{"data":{"token":"eyJ...","userServer":"uuid","roleId":"R001","isActive":true}}
๐ฅ Headers: Authorization: Bearer <token>
๐ค โ
Refresh (200): {"data":{"token":"eyJ...","expiresIn":2592000}}
๐ค โ
Verify (200):
{"data":{"userServer":"uuid","roleId":"R001","isActive":true,"expiresIn":2500000}}
โ Error: 401 (invalid/expired), 403 (akun dinonaktifkan)
Mengirim data PENDING dari database lokal device ke server. Endpoint ini menangani
insert baru, update, soft delete, dan
conflict resolution secara otomatis dalam satu transaksi ACID.
| Field | Type | Required | Deskripsi |
|---|---|---|---|
table |
string | Yes | Nama tabel target. Harus terdaftar di SYNCABLE_TABLES (whitelist). Tabel
master seperti users, roles, jabatan ditolak otomatis demi keamanan. |
records |
array | Yes | Array objek data lokal. Maksimal 100 record per request untuk menghindari timeout server. |
409 FK Violation.updatedAt wajib lebih baru dari versi server agar diterima. Jika
lebih lama/sama โ masuk conflicts.kwitansi & kwitansiCode
kosong, server generate otomatis [SEQ]/KWT/BCL/[MM]/[YY].client.updatedAt vs
server.updatedAt. Jika client <= server โ tandai
CONFLICT.INSERT atau UPDATE +
set syncStatus='SYNCED', lastSyncedAt=NOW().BEGIN TRANSACTION.
Error โ ROLLBACK total.โ Success (200):
{
"status": 200,
"message": "Push processed",
"data": {
"synced": ["550e8400-e29b-41d4-a716-446655440050"],
"conflicts": [{ "uuid": "550e8400-...", "serverData": { "updatedAt": "...", "deletedAt": null } }]
}
}
synced[].Tarik data server yang berubah sejak timestamp terakhir. Tidak menyertakan record soft-deleted.
๐ฅ Query: ?sinceTimestamp=2026-05-01 00:00:00
๐ค โ
(200):
{"data":{"records":{"attendance":[...],"kwitansi":[...]},"nextPullTimestamp":"..."}}
AND deletedAt IS NULL untuk tabel soft-delete. Tabel kwitansi dikecualikan
karena menggunakan hard-delete. Tabel master (users, roles, jabatan) tidak dipull via
endpoint ini.CRUD User Management (SuperAdmin Only). TIDAK termasuk dalam sync push/pull. Password otomatis didekripsi saat ambil detail.
GET /api/users?page=1&limit=20&search=budi&active=true
๐ค โ
(200): {"data":[{"userServer":"uuid","nama":"Budi","isActive":true}], "pagination":{...}}
GET /api/users?id=<uuid>
๐ค โ (200):
{
"status": 200,
"message": "User detail retrieved successfully",
"data": {
"userServer": "550e8400-...",
"nik": "3271009999999999",
"username": "karyawan.baru",
"password": "123456", โ โ
Plain text (auto-decrypt)
"passwordHash": "U2FsdGVkX1+...", โ Tetap ada untuk sync
"roleId": "R004",
"jabatanId": "J004",
"nama": "Ahmad Fauzi",
"handphone": "081299998888",
"email": "ahmad@test.com",
"dailySalary": 150000,
"isActive": true,
"createdAt": "2026-05-20 10:00:00",
"updatedAt": "2026-05-20 10:00:00"
}
}
/api/users โ Create user baru (password dienkripsi otomatis)/api/users?id=<uuid> โ Update partial field/api/users?id=<uuid>[&type=hard] โ Soft delete (default) atau hard deleteR001 yang bisa akses endpoint ini.logs/sync_*.log.ENCRYPTION_KEY di .env sama persis dengan saat password dienkripsi.Tabel kwitansi tidak akan dihapus (no soft/hard delete). Semua
penomoran ditangani sepenuhnya di sisi aplikasi.
[SEQ]/KWT/BCL/[MM]/[YY]
Contoh: 001/KWT/BCL/05/26, 002/KWT/BCL/05/26...
MAX(sequence) bulan berjalan di local DB sebelum
generate.409 Duplicate saat push, client harus increment sequence & retry.
{
"table": "kwitansi",
"records": [{
"kwitansiServer": "550e8400-...",
"milestoneServer": "550e8400-...",
"kwitansiCode": "001/KWT/BCL/05/26", โ Generated by App
"tanggal": "2026-05-20",
"nominal": 5000000,
"isLunas": false,
"syncStatus": "PENDING",
"updatedAt": "2026-05-20 10:00:00"
}]
}
/api/sse/timezone.php
8.1+ aktif di hPanelcomposer install --no-dev --optimize-autoloader selesai.env dibuat manual (JANGAN commit). Kunci: JWT_SECRET,
ENCRYPTION_KEYlogs/ & uploads/ writable (chmod 755).htaccess aktif: blokir .env, logs/, force clean URL
roles, jabatan, users
terisiAsia/Jakarta atau UTC).htaccess di uploads/ blokir eksekusi PHP &
directory listingJWT_SECRET & ENCRYPTION_KEY berkala. Monitoring logs/ untuk
anomali login atau sync conflict massal. Gunakan WorkManager di Android untuk trigger
sync background.