|
|
|
|
@@ -9,6 +9,11 @@ interface RoleOption {
|
|
|
|
|
description?: string | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ApiKeyPerms {
|
|
|
|
|
can_reset_self: boolean
|
|
|
|
|
can_reset_any: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function UsersPage() {
|
|
|
|
|
const { user } = useAuth()
|
|
|
|
|
const isAdmin = user?.is_admin === true
|
|
|
|
|
@@ -19,6 +24,8 @@ export default function UsersPage() {
|
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
const [message, setMessage] = useState('')
|
|
|
|
|
const [selectedId, setSelectedId] = useState<number | null>(null)
|
|
|
|
|
const [apikeyPerms, setApikeyPerms] = useState<ApiKeyPerms>({ can_reset_self: false, can_reset_any: false })
|
|
|
|
|
const [generatedApiKey, setGeneratedApiKey] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
const [createForm, setCreateForm] = useState({
|
|
|
|
|
username: '',
|
|
|
|
|
@@ -51,6 +58,7 @@ export default function UsersPage() {
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!selectedUser) return
|
|
|
|
|
setGeneratedApiKey(null)
|
|
|
|
|
setEditForm({
|
|
|
|
|
email: selectedUser.email,
|
|
|
|
|
full_name: selectedUser.full_name || '',
|
|
|
|
|
@@ -71,10 +79,12 @@ export default function UsersPage() {
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const [usersRes, rolesRes] = await Promise.all([
|
|
|
|
|
const [usersRes, rolesRes, apikeyRes] = await Promise.all([
|
|
|
|
|
api.get<User[]>('/users'),
|
|
|
|
|
api.get<RoleOption[]>('/roles'),
|
|
|
|
|
api.get<ApiKeyPerms>('/auth/me/apikey-permissions').catch(() => ({ data: { can_reset_self: false, can_reset_any: false } })),
|
|
|
|
|
])
|
|
|
|
|
setApikeyPerms(apikeyRes.data)
|
|
|
|
|
const assignableRoles = rolesRes.data
|
|
|
|
|
.filter((role) => role.name !== 'admin')
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
|
|
|
@@ -152,6 +162,29 @@ export default function UsersPage() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canResetApiKey = (targetUser: User) => {
|
|
|
|
|
if (apikeyPerms.can_reset_any) return true
|
|
|
|
|
if (apikeyPerms.can_reset_self && targetUser.id === user?.id) return true
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleResetApiKey = async () => {
|
|
|
|
|
if (!selectedUser) return
|
|
|
|
|
if (!confirm(`Reset API key for ${selectedUser.username}? The old key will be deactivated.`)) return
|
|
|
|
|
setSaving(true)
|
|
|
|
|
setMessage('')
|
|
|
|
|
setGeneratedApiKey(null)
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await api.post(`/users/${selectedUser.id}/reset-apikey`)
|
|
|
|
|
setGeneratedApiKey(data.api_key)
|
|
|
|
|
setMessage('API key reset successfully. Copy it now — it will not be shown again.')
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
setMessage(err.response?.data?.detail || 'Failed to reset API key')
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteUser = async () => {
|
|
|
|
|
if (!selectedUser) return
|
|
|
|
|
if (!confirm(`Delete user ${selectedUser.username}? This cannot be undone.`)) return
|
|
|
|
|
@@ -274,7 +307,7 @@ export default function UsersPage() {
|
|
|
|
|
<button className="btn-primary" disabled={saving} onClick={handleSaveUser}>
|
|
|
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-danger" disabled={saving || user?.id === selectedUser.id} onClick={handleDeleteUser}>
|
|
|
|
|
<button className="btn-danger" disabled={saving || user?.id === selectedUser.id || selectedUser.is_admin || selectedUser.username === 'acc-mgr'} onClick={handleDeleteUser}>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -326,6 +359,28 @@ export default function UsersPage() {
|
|
|
|
|
Active
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{canResetApiKey(selectedUser) && (
|
|
|
|
|
<div style={{ marginTop: '8px', padding: '12px', border: '1px solid var(--border)', borderRadius: '8px' }}>
|
|
|
|
|
<div style={{ fontWeight: 600, marginBottom: '10px' }}>API Key</div>
|
|
|
|
|
<button className="btn-secondary" disabled={saving} onClick={handleResetApiKey}>
|
|
|
|
|
🔑 Reset API Key
|
|
|
|
|
</button>
|
|
|
|
|
{generatedApiKey && (
|
|
|
|
|
<div style={{ marginTop: 10, padding: '10px', background: 'rgba(16,185,129,.08)', border: '1px solid rgba(16,185,129,.35)', borderRadius: '6px', wordBreak: 'break-all', fontFamily: 'monospace', fontSize: '13px' }}>
|
|
|
|
|
<div style={{ marginBottom: 6, fontWeight: 600, fontFamily: 'inherit' }}>New API Key (copy now!):</div>
|
|
|
|
|
<code>{generatedApiKey}</code>
|
|
|
|
|
<button
|
|
|
|
|
className="btn-sm"
|
|
|
|
|
style={{ marginLeft: 8 }}
|
|
|
|
|
onClick={() => { navigator.clipboard.writeText(generatedApiKey); setMessage('API key copied to clipboard') }}
|
|
|
|
|
>
|
|
|
|
|
📋 Copy
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
|