Login & Akses
Ringkasan
Section titled “Ringkasan”Login adalah pintu masuk dashboard. User menukar email dan password dengan sepasang token yang disimpan di browser, lalu aplikasi memanggil profil user untuk tahu peran akunnya. Peran itu yang menentukan menu dan halaman apa yang benar-benar dirender setelah masuk. Ada dua jalur masuk: login biasa memakai akun dashboard, dan login HRIS yang memakai akun kantor (Cognito) untuk akun yang sudah ditautkan — jalur kedua sedang dimatikan di kode.
Siapa yang pakai
Section titled “Siapa yang pakai”- Semua karyawan yang butuh dashboard — sekali per sesi, atau setiap kali token kedaluwarsa dan refresh gagal.
- Admin / HR — saat akun kantor belum tertaut, user diarahkan ke halaman “Akses Ditolak” dan penautan dilakukan admin.
flowchart TD
A[Buka alamat dashboard] --> B{Punya accessToken dan refreshToken?}
B -- ya --> C[Muat profil user]
B -- tidak --> D[Halaman Login]
D --> E[Isi email dan password, tekan Login]
E --> F[Kirim ke auth/login]
F --> G{Server menerima?}
G -- ya --> H[Simpan token, reload ke garis miring]
G -- tidak --> I[Toast error, tetap di halaman Login]
H --> C
C --> J{Peran boleh buka dashboard?}
J -- ya --> K[Dashboard terbuka]
J -- tidak --> L[Menu dan halaman yang diizinkan saja]
- Alamat dashboard dibuka; aplikasi memeriksa apakah browser sudah punya sesi.
- Tanpa sesi, user mendarat di halaman Login dan mengisi email serta password.
- Submit mengirim kredensial ke server; berhasil berarti token disimpan lalu halaman dimuat ulang ke
/. - Aplikasi memuat profil user, lalu menyaring rute dan menu sesuai peran.
/diteruskan ke/dashboard; kalau peran tidak berhak, dashboard tidak dirender dan user hanya melihat menu yang diizinkan.
Untuk pemakai
Section titled “Untuk pemakai”- Buka alamat dashboard. Kalau muncul halaman Login dengan judul besar “Login”, artinya sesi belum ada atau sudah habis.
- Isi Email (lengkap dengan
@) dan Password. Ikon mata di kanan kolom password menampilkan isinya sementara. - Tekan tombol Login. Tombol berubah jadi indikator loading selama permintaan berjalan.
- Berhasil: halaman dimuat ulang sebentar, lalu masuk ke Dashboard dan menu di sisi kiri muncul.
- Gagal: muncul notifikasi merah di pojok berisi pesan dari server (mis. password salah), form tetap terisi — perbaiki lalu tekan Login lagi.
Kalau gagal: halaman “Akses Ditolak” (bukan pesan error di form) berarti akun kantor belum ditautkan ke akun dashboard. Jangan mendaftar sendiri; admin yang menautkan. Tombol Kembali ke Halaman Login mengembalikan ke form login.
Layar & form
Section titled “Layar & form”Halaman Login (/auth/login)
Section titled “Halaman Login (/auth/login)”| Isian | Kendali | Wajib | Validasi | Catatan |
|---|---|---|---|---|
input[type=email] |
tidak | format email (browser) | id=login-email, ikon user, placeholder “Email” |
|
| Password | input[type=password] |
tidak | tidak ada, tanpa minLength |
id=login-password, ikon gembok |
| Tampilkan password | tombol ikon FaEye / FaEyeSlash |
— | — | type=button; hanya ganti type input |
| Login | button[type=submit] |
— | tidak pernah disabled |
jadi loading saat proses (useIsMutating) |
| Login with HRIS | button[type=button] |
— | — | tidak tampil selama ENABLE_HRIS_LOGIN = false |
Tidak ada tautan “lupa password”, tidak ada “ingat saya”, dan tidak ada pilihan bahasa di layar ini.
Apa yang terjadi saat submit
Section titled “Apa yang terjadi saat submit”sequenceDiagram autonumber participant U as User participant F as Form participant S as Server U->>F: klik Login F->>S: POST auth/login S-->>F: 200, 2 token F->>F: simpan token F->>U: href = / U->>S: GET user/me S-->>U: profil + role
- Klik Login memanggil
handleSubmit()disrc/pages/auth/Login.tsx, yang meneruskan{ email, password }kedoLogin(useLogin). useLogin(src/features/auth/hooks/api/useLogin.ts) menjalankanuseMutation, denganretry: 0dari pengaturan global disrc/context/ReactQuery.tsx.login()disrc/features/auth/api.tsmemanggilaxiosInstance.post("auth/login", { email, password }), base URLAPI_URL.- Interceptor request di
src/utils/axiosInstance.tsselalu menempelkanAuthorization: Bearer <accessToken>— pada login pertama token belum ada, jadi header terkirim sebagaiBearer null. - Server membalas
{ accessToken, refreshToken }(entity/Login.ts). onSuccessmenyimpanaccessToken,refreshToken, danauthProvider = "custom"kelocalStorage, lalu menjalankanwindow.location.href = "/"— reload penuh, bukan routing React.- Saat aplikasi hidup lagi:
src/index.tsxmenjalankanclearLegacySession()(tidak menghapus apa pun karenaauthProvidersudah ada), laluhandleCognitoRedirect()yang langsung mengembalikanfalsekarena tidak ada?code, laluAppdi-mount. PrivateRoute(src/context/PrivateRoute.tsx) melihataccessTokendanrefreshTokenada, jadiTheLayoutdirender.TheContentmemanggiluseGetMe()→GET user/me. Selama permintaan berjalan layar menampilkan spinner penuh, bukan form.- Rute disaring: hanya rute yang
roles-nya memuatuser.role(atau rute tanparoles) yang didaftarkan, lalu<Redirect from="/" to="/dashboard">membuka Dashboard.
Temuan saat dokumentasi: tombol Login memanggil handleSubmit() dua kali — sekali dari onClick tombol (SubmitButton) dan sekali lagi dari onSubmit pada CForm. Akibatnya satu klik mengirim dua permintaan POST auth/login, kecuali browser menahan event submit karena format email tidak valid (pada kasus itu permintaan dari onClick tetap jalan). Belum diperbaiki.
Validasi
Section titled “Validasi”- Di klien: hanya
type="email"pada kolom email, dan itu pun hanya dicek browser saat event submit form berjalan. Tidak adarequired,minLength,pattern, atau pengecekan kosong. Tombol Login tidak pernahdisabled, jadi submit dengan kedua kolom kosong tetap mengirim permintaan ke server. - Di server: penolakan kredensial datang sebagai respons error; pesannya diambil dari
err.response.data.messageatauerr.response.data.errorlalu ditampilkan sebagai toast olehonErrorglobal mutation. - Tidak divalidasi di mana pun: panjang minimum password, format email yang lebih ketat, dan pembatasan percobaan login berulang (tidak ada di sisi klien).
Redirect & navigasi
Section titled “Redirect & navigasi”| Kondisi | Tujuan |
|---|---|
| Login berhasil | / lalu <Redirect from="/" to="/dashboard"> — diawali reload penuh (window.location.href) |
Buka halaman dalam tanpa accessToken atau tanpa refreshToken |
/auth/login setelah localStorage.clear() (PrivateRoute) |
Sesi Cognito kena 401/403 |
/#/auth/not-registered — halaman “Akses Ditolak” (rejectSession) |
| Access token kedaluwarsa, refresh gagal | /#/auth/login setelah localStorage.clear() (interceptor axiosInstance) |
Login berhasil tapi cognitoId kosong dan ENABLE_HRIS_LOGIN aktif |
langsung diarahkan ke Cognito /login/continue (useGetMe → onSuccess) |
| Tombol “Kembali ke Halaman Login” di halaman Akses Ditolak | /auth/login lewat history.replace |
| Access token ada, refresh token dihapus manual | dianggap belum login → /auth/login |
Error yang muncul
Section titled “Error yang muncul”| Kondisi | Yang dilihat user | Penanganan di kode |
|---|---|---|
| Email/password salah (respons 4xx dengan body) | toast merah berisi message/error dari server, tetap di halaman Login |
onError global mutation di ReactQuery.tsx |
| Server mati / jaringan putus | toast error tanpa teks (kedua sumber pesan kosong) | toast.error(err?.response?.data?.message || err?.response?.data?.error) menghasilkan undefined |
| Refresh token gagal karena jaringan (bukan 4xx) | berpotensi layar diam | axiosInstance.ts membaca err.response.status tanpa memastikan err.response ada — bisa TypeError di dalam catch |
| Sesi Cognito tidak cocok dengan user mana pun (403) | halaman “Akses Ditolak”, bukan pesan izin | isTerminalAuthError() → rejectSession() |
Profil user/me gagal dimuat |
spinner berhenti, semua rute ber-roles tidak terdaftar |
user kosong → hasAccess false untuk semua rute berperan |
Untuk developer
Section titled “Untuk developer”| Hal | Keterangan |
|---|---|
| Layar | src/pages/auth/Login.tsx, src/pages/auth/NotRegistered.tsx |
| Logika | src/features/auth/ (api.ts, cognito.ts, hooks/api/useLogin.ts), src/utils/authSession.ts, src/utils/axiosInstance.ts, src/context/PrivateRoute.tsx |
| Server | POST auth/login, POST auth/access-token, GET user/me, PATCH ${API_URL_FINANCE}/api/user/me/cognito-link |
Aturan teknis:
- Dua token, dua fungsi.
accessTokenuntuk setiap permintaan;refreshTokenhanya untuk meminta access token baru lewatPOST auth/access-tokendengan headerx-refresh-token. authProvidermenentukan skema header.authScheme()mengembalikanCognitountuk sesi Cognito danBeareruntuk selainnya. Login biasa menulis"custom".- 401 ditangani berantai. Interceptor menahan permintaan, meminta token baru, lalu mengulang permintaan asli dengan
Bearer <token baru>; permintaan lain yang menunggu ikut memakai token yang sama (refreshSubscribers). - Jalur Cognito sedang mati.
ENABLE_HRIS_LOGIN = falsedisrc/config/featureFlags.ts. Selama false: tombol HRIS tidak dirender,handleCognitoRedirect()langsungfalse, danuseGetMetidak memaksa redirect ke Cognito. - Penautan akun ada di alur tukar token, bukan di halaman:
ensureCognitoLinked()mencobaGET user/medengan token Cognito, laluPATCH .../cognito-linkmemakaigetCognitoSub()dari payload JWT. - Sesi lama dibersihkan sekali di startup.
clearLegacySession()menghapuslocalStoragebila adaaccessTokentanpaauthProvider. - Penjagaan rute ada di klien.
TheContentmenyaring rute berdasarkanroute.roles.includes(user?.role);_nav.jsxmelakukan hal yang sama untuk menu. Server tetap harus menolak sendiri.
Relasi antar fitur
Section titled “Relasi antar fitur”- Butuh: Pengguna & Peran —
user.rolemenentukan menu dan rute yang dirender; tanpa data peran yang benar, user berhasil login tapi hampir semua menu hilang. - Terkait: HRIS / akun kantor — jalur Cognito menautkan akun kantor ke akun dashboard lewat
user/me/cognito-link.
Aturan & edge case
Section titled “Aturan & edge case”- Cukup salah satu token hilang (bukan keduanya) untuk dianggap belum login —
PrivateRoutemensyaratkan kedua token ada. 403pada sesi Cognito berakhir di halaman “belum terdaftar”, bukan pesan “akses ditolak” biasa; penyebabnya bisa akun belum ditautkan, bukan izin.- Token disimpan di
localStorage, jadi bisa dibaca skrip lain di origin yang sama. window.location.href = "/"memaksa reload penuh: state React hilang dan semua pemuatan awal diulang, termasukuser/me.GET user/meditahan dengan spinner penuh; kalau endpoint ini lambat, layar kosong sebelum dashboard muncul.
Riwayat
Section titled “Riwayat”- 2026-11-12 — dokumen dibuat dari
erdigmabranchmain@2e7ad26; alur, validasi, redirect, dan temuan ditelusuri langsung dari kode.