{"id":"cmv05qyr5000ltm4dfr8qnxkt","slug":"auth-anak","judul":"Auth Anak (PIN)","deskripsi":"Login anak via username + PIN, brute force protection","parentId":"cmv05qyqx000htm4dwawyj1ll","urut":2,"kategori":"backend","tag":"[\"auth\",\"security\"]","status":"aktif","versi":null,"konsep":"","createdAt":"2026-10-08T23:18:38.705Z","updatedAt":"2026-10-10T03:56:38.855Z","bagian":[{"id":"cmv05qyr5000mtm4dhvn2l0q3","blueprintId":"cmv05qyr5000ltm4dfr8qnxkt","judul":"Ringkasan","konten":"Login anak via username + PIN (bukan email/password).\n\n**Endpoint:** `POST /api/auth/native-anak`\n**Auth:** JWT bearer (30 hari expiry)\n**Pemakai:** App anak (zx-agent)\n\n**Keamanan:**\n- Rate limit: 5 percobaan/menit per username (in-memory Map)\n- Lockout: 10 gagal → 15 menit lock\n- PIN blacklist: 66 PIN lemah (sequential + repeated 4/5/6 digit)\n- Username: regex `/^[a-z0-9_]{3,20}$/`, 16 reserved words\n- bcrypt hash PIN\n- P2002 unique constraint catch → 409\n\n**File:**\n- `src/lib/validasi.ts` — USERNAME_REGEX, USERNAME_RESERVED, PIN_LEMAH, validasiUsername(), validasiPin(), duplikatUsername()\n- `src/app/api/auth/native-anak/route.ts` — login endpoint\n- `src/app/api/anak/[id]/pin/route.ts` — reset PIN\n- `src/lib/umur.ts` — hitungUmur() dari tanggalLahir","media":"[]","urut":0,"createdAt":"2026-10-08T23:18:38.705Z","updatedAt":"2026-10-08T23:29:57.188Z"},{"id":"cmv065ick0009pj2oanaw9l0g","blueprintId":"cmv05qyr5000ltm4dfr8qnxkt","judul":"PIN Blacklist","konten":"**66 PIN lemah di-blacklist:**\n\n**Repeated (30):**\n- 4-digit: `0000,1111,2222,...,9999`\n- 5-digit: `00000,11111,...,99999`\n- 6-digit: `000000,111111,...,999999`\n\n**Sequential naik (18):**\n- 4-digit: `0123,1234,2345,...,6789`\n- 5-digit: `01234,...,56789`\n- 6-digit: `012345,...,456789`\n\n**Sequential turun (18):**\n- 4-digit: `9876,8765,...,3210`\n- 5-digit: `98765,...,54321`\n- 6-digit: `987654,...,654321`\n\n**Catatan:**\n- `123456` = sequential naik → DITOLAK\n- `1234` = sequential naik → juga ada, tapi existing PIN tidak di-retroactive reject\n- User tidak bisa reset ke PIN yang ada di blacklist\n- Validasi: `validasiPin()` di `src/lib/validasi.ts`","media":"[]","urut":1,"createdAt":"2026-10-08T23:29:57.285Z","updatedAt":"2026-10-08T23:29:57.285Z"},{"id":"cmv065ih8000bpj2o9kxoojfo","blueprintId":"cmv05qyr5000ltm4dfr8qnxkt","judul":"Reset PIN Flow","konten":"**3 iterasi fix bug reset PIN:**\n\n**v2.9.5 — Iterasi 1:**\n- Bug: dialog fire-and-forget, tutup instan sebelum API respond\n- Fix: async callback `(sukses: Boolean) -> Unit` + loading spinner + error display via `state.error`\n\n**v2.9.6 — Iterasi 2:**\n- Bug: `state.error` global bisa ter-clear oleh operasi lain → user tidak lihat pesan error\n- Fix: callback `(sukses: Boolean, pesan: String?) -> Unit` — pesan langsung dari callback, tampilkan lokal di dialog sebagai `pesanPin` merah\n\n**v2.9.7 — Iterasi 3:**\n- Bug: reset PIN inline di `DialogEditAnak` → ambiguitas dua tombol Simpan (Simpan Edit + Simpan PIN Baru). User tekan tombol Simpan (edit) bukan Simpan PIN Baru → reset PIN tidak ter-trigger\n- Fix: pisah ke `DialogResetPin` composable terpisah dengan field PIN + tombol \"Simpan PIN Baru\" yang jelas\n\n**File:**\n- `LayarAnak.kt` — `DialogResetPin` composable, `DialogEditAnak` punya tombol \"Reset PIN\" yang buka dialog terpisah\n- `ZxVm.kt` — `resetPinAnak(id, pin, onSelesai: (sukses, pesan) -> Unit)`\n- `MainActivity.kt` — wiring `onResetPin = { id, pin, onSelesai -> vm.resetPinAnak(id, pin, onSelesai) }`\n- `Api.kt` — `anakResetPin @PATCH(\"anak/{id}/pin\")`, `AnakResetPinReq(val pin: String)`\n- Backend: `src/app/api/anak/[id]/pin/route.ts` — PATCH reset PIN, validasiPin, bcrypt hash","media":"[]","urut":2,"createdAt":"2026-10-08T23:29:57.452Z","updatedAt":"2026-10-08T23:29:57.452Z"},{"id":"cmv065ily000dpj2oa0ua74z3","blueprintId":"cmv05qyr5000ltm4dfr8qnxkt","judul":"Rate Limit & Lockout","konten":"**In-memory Map** (cukup single instance Coolify):\n\n```\nMap key = username (lowercase)\nValue = { gagal: number, lockUntil: number, recent: number[] }\n```\n\n**Rules:**\n- Max 5 percobaan per menit (sliding window)\n- 10 gagal total → lockout 15 menit\n- Sukses login → reset state\n- Lockout response: `429 { error: \"Terlalu banyak percobaan. Coba lagi dalam X menit.\" }`\n- Rate limit response: `429 { error: \"Terlalu banyak percobaan. Tunggu sebentar.\" }`\n\n**Catatan:** In-memory = reset jika container restart. Tidak persistent. Cukup untuk single VPS.","media":"[]","urut":3,"createdAt":"2026-10-08T23:29:57.622Z","updatedAt":"2026-10-08T23:29:57.622Z"}],"children":[],"parent":{"slug":"zx-parenting","judul":"ZX Parenting"}}