Replaces Firebase Auth with the new JWT + httpOnly-cookie refresh flow. Smoke-tested end-to-end via curl (login → /me → refresh rotation → logout). - Remove firebase dep + firebase.js - New token-bridge decouples api-client from AuthContext and de-dupes concurrent 401 refreshes - AuthContext: in-memory access token (useRef), bootstrap via /internal/auth/refresh, login/logout/refresh methods - api-client: withCredentials, Bearer attach, auto-retry once on 401 - LoginPage: handle INVALID_CREDENTIALS / ACCOUNT_LOCKED / VALIDATION_ERROR - Layout: self-service "Ganti password" form - UsersPage: initial password field on create + per-row admin-forced reset - .env / .env.example: drop VITE_FIREBASE_* vars - backend/CLAUDE.md + control_center/CLAUDE.md: describe new auth (were stale on Firebase) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.3 KiB
JavaScript
83 lines
3.3 KiB
JavaScript
import { useState } from 'react'
|
|
import { Outlet, NavLink } from 'react-router-dom'
|
|
import { useAuth } from '../core/auth/AuthContext'
|
|
import { apiClient } from '../core/api/api-client'
|
|
|
|
const PasswordChangeForm = ({ onDone }) => {
|
|
const [current, setCurrent] = useState('')
|
|
const [next, setNext] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
const submit = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
setSaving(true)
|
|
try {
|
|
await apiClient.patch('/internal/control-center-users/me/password', {
|
|
current_password: current,
|
|
new_password: next,
|
|
})
|
|
setSuccess(true)
|
|
setCurrent('')
|
|
setNext('')
|
|
} catch (err) {
|
|
const code = err?.response?.data?.error?.code
|
|
const msg = err?.response?.data?.error?.message
|
|
if (code === 'INVALID_CREDENTIALS') setError('Password saat ini salah.')
|
|
else if (code?.startsWith('PASSWORD_')) setError(msg || 'Password tidak memenuhi syarat.')
|
|
else setError('Gagal mengubah password.')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} style={{ padding: 8, border: '1px solid #eee', marginTop: 8 }}>
|
|
<input type="password" placeholder="Password lama" value={current}
|
|
onChange={e => setCurrent(e.target.value)} required
|
|
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
|
<input type="password" placeholder="Password baru (min 8, huruf besar/kecil + angka)" value={next}
|
|
onChange={e => setNext(e.target.value)} required minLength={8}
|
|
style={{ display: 'block', width: '100%', marginBottom: 6 }} />
|
|
{error && <p style={{ color: 'red', margin: '4px 0', fontSize: 12 }}>{error}</p>}
|
|
{success && <p style={{ color: 'green', margin: '4px 0', fontSize: 12 }}>Password berhasil diubah.</p>}
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<button type="submit" disabled={saving}>{saving ? '...' : 'Simpan'}</button>
|
|
<button type="button" onClick={onDone}>Tutup</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|
|
|
|
export default function Layout() {
|
|
const { user, logout } = useAuth()
|
|
const [showPwForm, setShowPwForm] = useState(false)
|
|
|
|
return (
|
|
<div style={{ display: 'flex', minHeight: '100vh' }}>
|
|
<nav style={{ width: 220, borderRight: '1px solid #eee', padding: 16 }}>
|
|
<h2>Control Center</h2>
|
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
|
<li><NavLink to="/dashboard">Dashboard</NavLink></li>
|
|
<li><NavLink to="/mitras">Mitra</NavLink></li>
|
|
<li><NavLink to="/sessions">Sesi</NavLink></li>
|
|
<li><NavLink to="/users">Users</NavLink></li>
|
|
<li><NavLink to="/mitra-activity">Aktivitas Mitra</NavLink></li>
|
|
<li><NavLink to="/settings">Settings</NavLink></li>
|
|
</ul>
|
|
<div style={{ marginTop: 'auto', paddingTop: 16 }}>
|
|
<p style={{ fontSize: 12 }}>{user?.email}</p>
|
|
<button onClick={() => setShowPwForm(v => !v)} style={{ marginRight: 6 }}>Ganti password</button>
|
|
<button onClick={logout}>Logout</button>
|
|
{showPwForm && <PasswordChangeForm onDone={() => setShowPwForm(false)} />}
|
|
</div>
|
|
</nav>
|
|
<main style={{ flex: 1, padding: 24 }}>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|