Add self-registration, role management, and department mapping UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e9cd40c80a
commit
fa32ea84ee
8 changed files with 584 additions and 7 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
import type { User } from '../types'
|
||||
import type { User, Role } from '../types'
|
||||
|
||||
interface ManagedUser {
|
||||
id: number
|
||||
|
|
@ -9,21 +9,26 @@ interface ManagedUser {
|
|||
active: boolean
|
||||
is_admin: boolean
|
||||
offsite_allowed: boolean
|
||||
workforce_user_id: string | null
|
||||
app_slugs: string[]
|
||||
roles: { id: number; name: string; slug: string }[]
|
||||
}
|
||||
|
||||
export function AdminUsers({ user }: { user: User }) {
|
||||
const [users, setUsers] = useState<ManagedUser[]>([])
|
||||
const [allApps, setAllApps] = useState<{ slug: string; name: string }[]>([])
|
||||
const [allRoles, setAllRoles] = useState<Role[]>([])
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
|
||||
async function load() {
|
||||
const [u, a] = await Promise.all([
|
||||
const [u, a, r] = await Promise.all([
|
||||
fetch('/api/auth/admin/users', { credentials: 'include' }).then(r => r.json()),
|
||||
fetch('/api/auth/admin/apps', { credentials: 'include' }).then(r => r.json()),
|
||||
fetch('/api/auth/admin/roles', { credentials: 'include' }).then(r => r.json()),
|
||||
])
|
||||
setUsers(u)
|
||||
setAllApps(a)
|
||||
setAllRoles(r)
|
||||
}
|
||||
|
||||
useEffect(() => { load() }, [])
|
||||
|
|
@ -44,6 +49,27 @@ export function AdminUsers({ user }: { user: User }) {
|
|||
load()
|
||||
}
|
||||
|
||||
async function addRole(userId: number, roleId: number) {
|
||||
await fetch(`/api/auth/admin/users/${userId}/roles/${roleId}`, {
|
||||
method: 'POST', credentials: 'include',
|
||||
})
|
||||
load()
|
||||
}
|
||||
|
||||
async function removeRole(userId: number, roleId: number) {
|
||||
await fetch(`/api/auth/admin/users/${userId}/roles/${roleId}`, {
|
||||
method: 'DELETE', credentials: 'include',
|
||||
})
|
||||
load()
|
||||
}
|
||||
|
||||
async function syncWfRoles(userId: number) {
|
||||
await fetch(`/api/auth/admin/workforce/sync-user/${userId}`, {
|
||||
method: 'POST', credentials: 'include',
|
||||
})
|
||||
load()
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', height: '100dvh' }}>
|
||||
<Sidebar user={user} />
|
||||
|
|
@ -75,6 +101,7 @@ export function AdminUsers({ user }: { user: User }) {
|
|||
<Toggle label="Offsite" on={u.offsite_allowed} onClick={() => toggle(u.id, 'offsite_allowed', u.offsite_allowed)} />
|
||||
</div>
|
||||
</div>
|
||||
{/* App grants */}
|
||||
<div style={{ marginTop: '0.75rem', display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
|
||||
{allApps.map(app => {
|
||||
const has = u.app_slugs.includes(app.slug)
|
||||
|
|
@ -90,6 +117,43 @@ export function AdminUsers({ user }: { user: User }) {
|
|||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Roles */}
|
||||
<div style={{ marginTop: '0.5rem', display: 'flex', gap: '0.4rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginRight: '0.15rem' }}>Roles:</span>
|
||||
{u.roles.map(r => (
|
||||
<button key={r.id} onClick={() => removeRole(u.id, r.id)} title="Click to remove" style={{
|
||||
background: 'var(--navy)', border: '1px solid var(--surface-2)',
|
||||
color: 'var(--text-muted)', borderRadius: '4px',
|
||||
padding: '0.15rem 0.5rem', fontSize: '0.72rem', cursor: 'pointer',
|
||||
}}>
|
||||
{r.name} ×
|
||||
</button>
|
||||
))}
|
||||
<select
|
||||
value=""
|
||||
onChange={e => { if (e.target.value) addRole(u.id, Number(e.target.value)) }}
|
||||
style={{
|
||||
background: 'var(--surface-2)', border: '1px solid var(--surface-2)',
|
||||
borderRadius: '4px', color: 'var(--text-muted)',
|
||||
padding: '0.15rem 0.4rem', fontSize: '0.72rem',
|
||||
}}
|
||||
>
|
||||
<option value="">+ Add role</option>
|
||||
{allRoles.filter(r => !u.roles.find(ur => ur.id === r.id)).map(r => (
|
||||
<option key={r.id} value={r.id}>{r.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{u.workforce_user_id && (
|
||||
<button onClick={() => syncWfRoles(u.id)} style={{
|
||||
background: 'none', border: '1px solid var(--surface-2)',
|
||||
color: 'var(--text-muted)', borderRadius: '4px',
|
||||
padding: '0.15rem 0.5rem', fontSize: '0.72rem', cursor: 'pointer',
|
||||
}}>
|
||||
Sync WF roles
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue