feat: add reporter role for SSO auto-provisioning
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m10s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 14s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m10s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 14s
CI Pipeline / Build .deb & Release (push) Has been skipped
This commit is contained in:
@ -29,6 +29,7 @@ interface NavItem {
|
||||
path: string
|
||||
icon: React.ReactElement
|
||||
adminOnly?: boolean
|
||||
writeOnly?: boolean
|
||||
}
|
||||
|
||||
const navGroups: { heading: string; items: NavItem[] }[] = [
|
||||
@ -43,23 +44,23 @@ const navGroups: { heading: string; items: NavItem[] }[] = [
|
||||
items: [
|
||||
{ label: 'Hosts', path: '/hosts', icon: <HostsIcon /> },
|
||||
{ label: 'Groups', path: '/groups', icon: <GroupsIcon /> },
|
||||
{ label: 'Deploy', path: '/deployment', icon: <DeployIcon /> },
|
||||
{ label: 'Deploy', path: '/deployment', icon: <DeployIcon />, writeOnly: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Operations',
|
||||
items: [
|
||||
{ label: 'Jobs', path: '/jobs', icon: <JobsIcon /> },
|
||||
{ label: 'Maintenance', path: '/maintenance', icon: <MaintenanceIcon /> },
|
||||
{ label: 'Maintenance', path: '/maintenance', icon: <MaintenanceIcon />, writeOnly: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Administration',
|
||||
items: [
|
||||
{ label: 'Users', path: '/users', icon: <UsersIcon />, adminOnly: true },
|
||||
{ label: 'Certificates', path: '/certificates', icon: <CertsIcon />, adminOnly: true },
|
||||
{ label: 'Certificates', path: '/certificates', icon: <CertsIcon /> },
|
||||
{ label: 'Reports', path: '/reports', icon: <ReportsIcon /> },
|
||||
{ label: 'Settings', path: '/settings', icon: <SettingsIcon />, adminOnly: true },
|
||||
{ label: 'Settings', path: '/settings', icon: <SettingsIcon /> },
|
||||
],
|
||||
},
|
||||
]
|
||||
@ -72,7 +73,7 @@ export default function AppLayout() {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
|
||||
const isAdmin = user?.role === 'admin'
|
||||
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const handleDrawerToggle = () => setMobileOpen(!mobileOpen)
|
||||
const handleMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget)
|
||||
const handleMenuClose = () => setAnchorEl(null)
|
||||
@ -97,7 +98,11 @@ export default function AppLayout() {
|
||||
<Divider />
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', py: 1 }}>
|
||||
{navGroups.map((group) => {
|
||||
const visibleItems = group.items.filter((item) => !item.adminOnly || isAdmin)
|
||||
const visibleItems = group.items.filter((item) => {
|
||||
if (item.adminOnly && !isAdmin) return false
|
||||
if (item.writeOnly && !canWrite) return false
|
||||
return true
|
||||
})
|
||||
if (visibleItems.length === 0) return null
|
||||
return (
|
||||
<Box key={group.heading} sx={{ mb: 1 }}>
|
||||
|
||||
@ -286,7 +286,7 @@ function KeyDisplayDialog({ open, cert, hostname, onClose }: KeyDisplayDialogPro
|
||||
|
||||
export default function CertificatesPage() {
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const isAdmin = user?.role === 'admin'
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
|
||||
const [certs, setCerts] = useState<Certificate[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -378,7 +378,7 @@ export default function CertificatesPage() {
|
||||
<Typography variant="h5" fontWeight={700} sx={{ flexGrow: 1 }}>
|
||||
Certificate Management
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
{canWrite && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<SecurityIcon />}
|
||||
@ -496,7 +496,7 @@ export default function CertificatesPage() {
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{isAdmin && (
|
||||
{canWrite && (
|
||||
<>
|
||||
<Tooltip title="Renew certificate">
|
||||
<Button
|
||||
|
||||
@ -6,9 +6,12 @@ import {
|
||||
} from '@mui/material'
|
||||
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'
|
||||
import { apiClient } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import type { Group } from '../types'
|
||||
|
||||
export default function GroupsPage() {
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -39,13 +42,13 @@ export default function GroupsPage() {
|
||||
<Container maxWidth="lg" sx={{ mt: 3 }}>
|
||||
<Toolbar disableGutters sx={{ mb: 2 }}>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ flexGrow: 1 }}>Groups</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpen(true)}>Create Group</Button>
|
||||
{canWrite && <Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpen(true)}>Create Group</Button>}
|
||||
</Toolbar>
|
||||
{loading ? <Box display="flex" justifyContent="center" mt={4}><CircularProgress /></Box> : (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead><TableRow>
|
||||
<TableCell>Name</TableCell><TableCell>Description</TableCell><TableCell>Created</TableCell><TableCell>Actions</TableCell>
|
||||
<TableCell>Name</TableCell><TableCell>Description</TableCell><TableCell>Created</TableCell>{canWrite && <TableCell>Actions</TableCell>}
|
||||
</TableRow></TableHead>
|
||||
<TableBody>
|
||||
{groups.map(g => (
|
||||
@ -53,9 +56,9 @@ export default function GroupsPage() {
|
||||
<TableCell sx={{ fontWeight: 600 }}>{g.name}</TableCell>
|
||||
<TableCell>{g.description || '—'}</TableCell>
|
||||
<TableCell>{new Date(g.created_at).toLocaleDateString()}</TableCell>
|
||||
<TableCell>
|
||||
{canWrite && <TableCell>
|
||||
<Tooltip title="Delete"><IconButton size="small" color="error" onClick={() => handleDelete(g.id)}><DeleteIcon fontSize="small" /></IconButton></Tooltip>
|
||||
</TableCell>
|
||||
</TableCell>}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@ -48,6 +48,7 @@ import {
|
||||
ContentCopy as CopyIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { apiClient, hostsApi, maintenanceWindowsApi, healthChecksApi, certsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import type {
|
||||
CreateHostRequest,
|
||||
IssuedCert,
|
||||
@ -552,6 +553,8 @@ function CreateHostForm() {
|
||||
export default function HostDetailPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [host, setHost] = useState<Record<string, unknown> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -896,7 +899,7 @@ export default function HostDetailPage() {
|
||||
{String(host?.fqdn ?? '')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{!certExists && (
|
||||
{canWrite && !certExists && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
@ -906,7 +909,7 @@ export default function HostDetailPage() {
|
||||
Issue Certificate
|
||||
</Button>
|
||||
)}
|
||||
{certExists && (
|
||||
{canWrite && certExists && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@ -942,14 +945,14 @@ export default function HostDetailPage() {
|
||||
<ScheduleIcon color="primary" />
|
||||
<Typography variant="h6" fontWeight={600}>Maintenance Windows</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
{canWrite && <Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => { setCreateForm(defaultForm()); setCreateOpen(true) }}
|
||||
>
|
||||
Add Window
|
||||
</Button>
|
||||
</Button>}
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
@ -971,7 +974,7 @@ export default function HostDetailPage() {
|
||||
<TableCell>Schedule</TableCell>
|
||||
<TableCell>Recurrence</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
{canWrite && <TableCell align="right">Actions</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -991,7 +994,7 @@ export default function HostDetailPage() {
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{canWrite && <TableCell align="right">
|
||||
<Tooltip title="Edit">
|
||||
<IconButton size="small" onClick={() => handleEditClick(w)}>
|
||||
<EditIcon fontSize="small" />
|
||||
@ -1005,7 +1008,7 @@ export default function HostDetailPage() {
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableCell>}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@ -1020,7 +1023,7 @@ export default function HostDetailPage() {
|
||||
<MonitorHeartIcon color="primary" />
|
||||
<Typography variant="h6" fontWeight={600}>Health Checks</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
{canWrite && <Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@ -1028,7 +1031,7 @@ export default function HostDetailPage() {
|
||||
onClick={() => { setHcCreateForm(defaultHealthCheckForm()); setHcCreateOpen(true) }}
|
||||
>
|
||||
Add Health Check
|
||||
</Button>
|
||||
</Button>}
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
@ -1054,7 +1057,7 @@ export default function HostDetailPage() {
|
||||
<TableCell>Detail</TableCell>
|
||||
<TableCell>Latency</TableCell>
|
||||
<TableCell>Last Checked</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
{canWrite && <TableCell align="right">Actions</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -1092,7 +1095,8 @@ export default function HostDetailPage() {
|
||||
<Switch
|
||||
size="small"
|
||||
checked={check.enabled}
|
||||
onChange={() => handleToggleEnabled(check)}
|
||||
onChange={canWrite ? () => handleToggleEnabled(check) : undefined}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@ -1108,7 +1112,7 @@ export default function HostDetailPage() {
|
||||
? new Date(check.last_result.checked_at).toLocaleString()
|
||||
: '—'}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{canWrite && <TableCell align="right">
|
||||
<Tooltip title="Test now">
|
||||
<IconButton
|
||||
size="small"
|
||||
@ -1134,7 +1138,7 @@ export default function HostDetailPage() {
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableCell>}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import { Add as AddIcon, Refresh as RefreshIcon, Delete as DeleteIcon, CheckCircle as CheckCircleIcon, Cancel as CancelIcon, Remove as RemoveIcon } from '@mui/icons-material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { apiClient, hostsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import type { Host, HostHealthStatus } from '../types'
|
||||
|
||||
const statusColor = (s: HostHealthStatus) =>
|
||||
@ -14,6 +15,8 @@ const statusColor = (s: HostHealthStatus) =>
|
||||
|
||||
export default function HostsPage() {
|
||||
const navigate = useNavigate()
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [hosts, setHosts] = useState<Host[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -55,7 +58,7 @@ export default function HostsPage() {
|
||||
<TextField size="small" placeholder="Search..." value={search}
|
||||
onChange={e => setSearch(e.target.value)} sx={{ mr: 2 }} />
|
||||
<Tooltip title="Refresh"><IconButton onClick={load}><RefreshIcon /></IconButton></Tooltip>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/hosts/new')} sx={{ ml: 1 }}>Add Host</Button>
|
||||
{canWrite && <Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/hosts/new')} sx={{ ml: 1 }}>Add Host</Button>}
|
||||
</Toolbar>
|
||||
{loading ? <Box display="flex" justifyContent="center" mt={4}><CircularProgress /></Box> : (
|
||||
<TableContainer component={Paper}>
|
||||
@ -69,7 +72,7 @@ export default function HostsPage() {
|
||||
<TableCell>Health</TableCell>
|
||||
<TableCell>Checks</TableCell>
|
||||
<TableCell>Agent</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
{canWrite && <TableCell>Actions</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -93,7 +96,7 @@ export default function HostsPage() {
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{h.agent_version ?? '—'}</TableCell>
|
||||
<TableCell onClick={e => e.stopPropagation()}>
|
||||
{canWrite && <TableCell onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="Request refresh">
|
||||
<IconButton size="small" color="primary"
|
||||
disabled={refreshing === h.id}
|
||||
@ -106,7 +109,7 @@ export default function HostsPage() {
|
||||
<Tooltip title="Delete"><IconButton size="small" color="error">
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton></Tooltip>
|
||||
</TableCell>
|
||||
</TableCell>}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
WifiOff as WifiOffIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { jobsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { useJobWebSocket } from '../hooks/useJobWebSocket'
|
||||
import type { JobStatus, JobKind, PatchJobSummary, PatchJob, PatchJobHost, JobWsEvent } from '../types'
|
||||
|
||||
@ -153,6 +154,7 @@ interface JobRowProps {
|
||||
detail: PatchJob | null
|
||||
detailLoading: boolean
|
||||
detailError: string | null
|
||||
canWrite: boolean
|
||||
}
|
||||
|
||||
function JobRow({
|
||||
@ -166,6 +168,7 @@ function JobRow({
|
||||
detail,
|
||||
detailLoading,
|
||||
detailError,
|
||||
canWrite,
|
||||
}: JobRowProps) {
|
||||
const canCancel = job.status === 'queued' || job.status === 'pending'
|
||||
const canRollback = job.status === 'succeeded'
|
||||
@ -224,7 +227,7 @@ function JobRow({
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Box display="flex" gap={0.5}>
|
||||
{canWrite ? <Box display="flex" gap={0.5}>
|
||||
{canCancel && (
|
||||
<Tooltip title="Cancel job">
|
||||
<span>
|
||||
@ -261,7 +264,7 @@ function JobRow({
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box> : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -289,6 +292,8 @@ function JobRow({
|
||||
|
||||
// ── JobsPage ──────────────────────────────────────────────────────────────────
|
||||
export default function JobsPage() {
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [jobs, setJobs] = useState<PatchJobSummary[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@ -512,7 +517,7 @@ export default function JobsPage() {
|
||||
<TableCell align="right">Failed</TableCell>
|
||||
<TableCell>Schedule</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
{canWrite && <TableCell>Actions</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -544,6 +549,7 @@ export default function JobsPage() {
|
||||
detail={details[job.id] ?? null}
|
||||
detailLoading={detailLoading[job.id] ?? false}
|
||||
detailError={detailError[job.id] ?? null}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@ -37,6 +37,7 @@ import {
|
||||
Schedule as ScheduleIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { maintenanceWindowsApi, hostsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import type { Host, MaintenanceWindow, WindowRecurrence } from '../types'
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@ -311,9 +312,10 @@ interface HostWindowsTableProps {
|
||||
onEdit: (w: MaintenanceWindow) => void
|
||||
onDelete: (w: MaintenanceWindow) => void
|
||||
onAdd: (hostId: string) => void
|
||||
canWrite: boolean
|
||||
}
|
||||
|
||||
function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindowsTableProps) {
|
||||
function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd, canWrite }: HostWindowsTableProps) {
|
||||
return (
|
||||
<Paper variant="outlined" sx={{ mb: 3 }}>
|
||||
<Box
|
||||
@ -336,14 +338,14 @@ function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindow
|
||||
({host.fqdn})
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
{canWrite && <Button
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => onAdd(host.id)}
|
||||
>
|
||||
Add Window
|
||||
</Button>
|
||||
</Button>}
|
||||
</Box>
|
||||
|
||||
{windows.length === 0 ? (
|
||||
@ -362,7 +364,7 @@ function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindow
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Auto-Apply</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
{canWrite && <TableCell align="right">Actions</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -394,7 +396,7 @@ function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindow
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{fmtDate(w.created_at)}</TableCell>
|
||||
<TableCell align="right">
|
||||
{canWrite && <TableCell align="right">
|
||||
<Tooltip title="Edit">
|
||||
<IconButton size="small" onClick={() => onEdit(w)}>
|
||||
<EditIcon fontSize="small" />
|
||||
@ -405,7 +407,7 @@ function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindow
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableCell>}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@ -418,6 +420,8 @@ function HostWindowsTable({ host, windows, onEdit, onDelete, onAdd }: HostWindow
|
||||
// ── Main page ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function MaintenanceWindowsPage() {
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [hosts, setHosts] = useState<Host[]>([])
|
||||
const [windowsByHost, setWindowsByHost] = useState<Record<string, MaintenanceWindow[]>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -593,9 +597,9 @@ export default function MaintenanceWindowsPage() {
|
||||
onEdit={handleEditClick}
|
||||
onDelete={handleDeleteClick}
|
||||
onAdd={handleAddClick}
|
||||
canWrite={canWrite}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Create dialog */}
|
||||
<WindowFormDialog
|
||||
open={createOpen}
|
||||
|
||||
@ -15,6 +15,7 @@ import EmailIcon from '@mui/icons-material/Email'
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey'
|
||||
import ExploreIcon from '@mui/icons-material/Explore'
|
||||
import { settingsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import type { OidcConfigResponse, OidcDiscoveryResult, SmtpConfig, PollingConfig, NotificationConfig } from '../types'
|
||||
|
||||
type OidcForm = OidcConfigResponse & { client_secret?: string }
|
||||
@ -23,6 +24,8 @@ type SmtpForm = SmtpConfig & { password?: string }
|
||||
const KEYCLOAK_DISCOVERY_URL = 'https://keycloak.moon-dragon.us/realms/moon-dragon.us/.well-known/openid-configuration'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const user = useAuthStore(state => state.user)
|
||||
const canWrite = user?.role === 'admin' || user?.role === 'operator'
|
||||
const [oidc, setOidc] = useState<OidcForm>({
|
||||
enabled: false, provider_type: 'azure', display_name: 'Azure AD',
|
||||
discovery_url: '', client_id: '', client_secret: '', redirect_uri: '', scopes: 'openid profile email',
|
||||
@ -202,9 +205,9 @@ export default function SettingsPage() {
|
||||
<Container maxWidth="lg" sx={{ mt: 3 }}>
|
||||
<Toolbar disableGutters sx={{ mb: 3, justifyContent: 'space-between' }}>
|
||||
<Typography variant="h5" fontWeight={700}>Settings</Typography>
|
||||
<Button variant="contained" onClick={handleSave} disabled={saving} startIcon={saving ? <CircularProgress size={20} /> : <SaveIcon />}>
|
||||
{canWrite && <Button variant="contained" onClick={handleSave} disabled={saving} startIcon={saving ? <CircularProgress size={20} /> : <SaveIcon />}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</Button>}
|
||||
</Toolbar>
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>{error}</Alert>}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Core TypeScript types — expanded per milestone
|
||||
|
||||
export type UserRole = 'admin' | 'operator'
|
||||
export type UserRole = 'admin' | 'operator' | 'reporter'
|
||||
export type AuthProvider = 'local' | 'azure_sso' | 'keycloak' | 'oidc'
|
||||
export type HostHealthStatus = 'pending' | 'healthy' | 'degraded' | 'unreachable'
|
||||
export type JobStatus = 'queued' | 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled'
|
||||
|
||||
Reference in New Issue
Block a user