Skip to content

Login & Akses

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.

  • 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]
  1. Alamat dashboard dibuka; aplikasi memeriksa apakah browser sudah punya sesi.
  2. Tanpa sesi, user mendarat di halaman Login dan mengisi email serta password.
  3. Submit mengirim kredensial ke server; berhasil berarti token disimpan lalu halaman dimuat ulang ke /.
  4. Aplikasi memuat profil user, lalu menyaring rute dan menu sesuai peran.
  5. / diteruskan ke /dashboard; kalau peran tidak berhak, dashboard tidak dirender dan user hanya melihat menu yang diizinkan.
  1. Buka alamat dashboard. Kalau muncul halaman Login dengan judul besar “Login”, artinya sesi belum ada atau sudah habis.
  2. Isi Email (lengkap dengan @) dan Password. Ikon mata di kanan kolom password menampilkan isinya sementara.
  3. Tekan tombol Login. Tombol berubah jadi indikator loading selama permintaan berjalan.
  4. Berhasil: halaman dimuat ulang sebentar, lalu masuk ke Dashboard dan menu di sisi kiri muncul.
  5. 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.

Isian Kendali Wajib Validasi Catatan
Email 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.

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
  1. Klik Login memanggil handleSubmit() di src/pages/auth/Login.tsx, yang meneruskan { email, password } ke doLogin (useLogin).
  2. useLogin (src/features/auth/hooks/api/useLogin.ts) menjalankan useMutation, dengan retry: 0 dari pengaturan global di src/context/ReactQuery.tsx.
  3. login() di src/features/auth/api.ts memanggil axiosInstance.post("auth/login", { email, password }), base URL API_URL.
  4. Interceptor request di src/utils/axiosInstance.ts selalu menempelkan Authorization: Bearer <accessToken> — pada login pertama token belum ada, jadi header terkirim sebagai Bearer null.
  5. Server membalas { accessToken, refreshToken } (entity/Login.ts).
  6. onSuccess menyimpan accessToken, refreshToken, dan authProvider = "custom" ke localStorage, lalu menjalankan window.location.href = "/" — reload penuh, bukan routing React.
  7. Saat aplikasi hidup lagi: src/index.tsx menjalankan clearLegacySession() (tidak menghapus apa pun karena authProvider sudah ada), lalu handleCognitoRedirect() yang langsung mengembalikan false karena tidak ada ?code, lalu App di-mount.
  8. PrivateRoute (src/context/PrivateRoute.tsx) melihat accessToken dan refreshToken ada, jadi TheLayout dirender.
  9. TheContent memanggil useGetMe() → GET user/me. Selama permintaan berjalan layar menampilkan spinner penuh, bukan form.
  10. Rute disaring: hanya rute yang roles-nya memuat user.role (atau rute tanpa roles) 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.

  • Di klien: hanya type="email" pada kolom email, dan itu pun hanya dicek browser saat event submit form berjalan. Tidak ada required, minLength, pattern, atau pengecekan kosong. Tombol Login tidak pernah disabled, 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.message atau err.response.data.error lalu ditampilkan sebagai toast oleh onError global mutation.
  • Tidak divalidasi di mana pun: panjang minimum password, format email yang lebih ketat, dan pembatasan percobaan login berulang (tidak ada di sisi klien).
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
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
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. accessToken untuk setiap permintaan; refreshToken hanya untuk meminta access token baru lewat POST auth/access-token dengan header x-refresh-token.
  • authProvider menentukan skema header. authScheme() mengembalikan Cognito untuk sesi Cognito dan Bearer untuk 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 = false di src/config/featureFlags.ts. Selama false: tombol HRIS tidak dirender, handleCognitoRedirect() langsung false, dan useGetMe tidak memaksa redirect ke Cognito.
  • Penautan akun ada di alur tukar token, bukan di halaman: ensureCognitoLinked() mencoba GET user/me dengan token Cognito, lalu PATCH .../cognito-link memakai getCognitoSub() dari payload JWT.
  • Sesi lama dibersihkan sekali di startup. clearLegacySession() menghapus localStorage bila ada accessToken tanpa authProvider.
  • Penjagaan rute ada di klien. TheContent menyaring rute berdasarkan route.roles.includes(user?.role); _nav.jsx melakukan hal yang sama untuk menu. Server tetap harus menolak sendiri.
  • Butuh: Pengguna & Peran — user.role menentukan 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.
  • Cukup salah satu token hilang (bukan keduanya) untuk dianggap belum login — PrivateRoute mensyaratkan kedua token ada.
  • 403 pada 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, termasuk user/me.
  • GET user/me ditahan dengan spinner penuh; kalau endpoint ini lambat, layar kosong sebelum dashboard muncul.
  • 2026-11-12 — dokumen dibuat dari erdigma branch main @ 2e7ad26; alur, validasi, redirect, dan temuan ditelusuri langsung dari kode.