From 96116dbfe4e0d89ff7d3c0fa83278d2e48de0846 Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Sun, 12 Apr 2026 20:31:47 +0200 Subject: [PATCH] feat: display elevation in the clients page --- ui/src/client/ClientStore.ts | 7 +++ ui/src/client/Clients.tsx | 36 +++++++++++- ui/src/client/ElevateClientDialog.tsx | 83 +++++++++++++++++++++++++++ ui/src/tests/client.test.ts | 6 +- ui/src/types.ts | 1 + 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 ui/src/client/ElevateClientDialog.tsx diff --git a/ui/src/client/ClientStore.ts b/ui/src/client/ClientStore.ts index cc63b6e..4b642d8 100644 --- a/ui/src/client/ClientStore.ts +++ b/ui/src/client/ClientStore.ts @@ -38,4 +38,11 @@ export class ClientStore extends BaseStore { await this.createNoNotifcation(name); this.snack('Client added'); }; + + @action + public elevate = async (id: number, durationSeconds: number): Promise => { + await axios.post(`${config.get('url')}client:elevate`, {id, durationSeconds}); + await this.refresh(); + this.snack('Client elevated'); + }; } diff --git a/ui/src/client/Clients.tsx b/ui/src/client/Clients.tsx index 9ff573f..e7e1f1a 100644 --- a/ui/src/client/Clients.tsx +++ b/ui/src/client/Clients.tsx @@ -9,14 +9,19 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Delete from '@mui/icons-material/Delete'; import Edit from '@mui/icons-material/Edit'; +import Security from '@mui/icons-material/Security'; import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; +import TimeAgo from 'react-timeago'; import ConfirmDialog from '../common/ConfirmDialog'; import DefaultPage from '../common/DefaultPage'; import AddClientDialog from './AddClientDialog'; import UpdateClientDialog from './UpdateClientDialog'; +import ElevateClientDialog from './ElevateClientDialog'; import {IClient} from '../types'; import CopyableSecret from '../common/CopyableSecret'; import {LastUsedCell} from '../common/LastUsedCell'; +import {TimeAgoFormatter} from '../common/TimeAgoFormatter'; import {observer} from 'mobx-react-lite'; import {useStores} from '../stores'; @@ -24,6 +29,7 @@ const Clients = observer(() => { const {clientStore} = useStores(); const [toDeleteClient, setToDeleteClient] = useState(); const [toUpdateClient, setToUpdateClient] = useState(); + const [toElevateClient, setToElevateClient] = useState(); const [createDialog, setCreateDialog] = useState(false); const clients = clientStore.getItems(); @@ -32,6 +38,7 @@ const Clients = observer(() => { return ( { Name Token Last Used + Elevation ends + @@ -60,8 +69,10 @@ const Clients = observer(() => { name={client.name} value={client.token} lastUsed={client.lastUsed} + elevatedUntil={client.elevatedUntil} fEdit={() => setToUpdateClient(client)} fDelete={() => setToDeleteClient(client)} + fElevate={() => setToElevateClient(client)} /> ))} @@ -90,6 +101,13 @@ const Clients = observer(() => { requireElevated /> )} + {toElevateClient != null && ( + setToElevateClient(undefined)} + /> + )} ); }); @@ -98,11 +116,13 @@ interface IRowProps { name: string; value: string; lastUsed: string | null; + elevatedUntil?: string; fEdit: VoidFunction; fDelete: VoidFunction; + fElevate: VoidFunction; } -const Row = ({name, value, lastUsed, fEdit, fDelete}: IRowProps) => ( +const Row = ({name, value, lastUsed, elevatedUntil, fEdit, fDelete, fElevate}: IRowProps) => ( {name} @@ -114,6 +134,20 @@ const Row = ({name, value, lastUsed, fEdit, fDelete}: IRowProps) => ( + + {elevatedUntil && Date.parse(elevatedUntil) > Date.now() ? ( + + ) : ( + '-' + )} + + + + + + + + diff --git a/ui/src/client/ElevateClientDialog.tsx b/ui/src/client/ElevateClientDialog.tsx new file mode 100644 index 0000000..e868a05 --- /dev/null +++ b/ui/src/client/ElevateClientDialog.tsx @@ -0,0 +1,83 @@ +import React, {useState} from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import {observer} from 'mobx-react-lite'; +import {useStores} from '../stores'; +import ElevationForm from '../common/ElevationForm'; + +interface IProps { + clientName: string; + clientId: number; + fClose: VoidFunction; +} + +const durationOptions = [ + {label: 'Cancel elevation', seconds: -1}, + {label: '1 hour', seconds: 60 * 60}, + {label: '1 day', seconds: 24 * 60 * 60}, + {label: '30 days', seconds: 30 * 24 * 60 * 60}, + {label: '1 year', seconds: 365 * 24 * 60 * 60}, +]; + +const ElevateClientDialog = observer(({clientName, clientId, fClose}: IProps) => { + const {elevateStore, clientStore, currentUser} = useStores(); + const [durationSeconds, setDurationSeconds] = useState(durationOptions[1].seconds); + + const needsElevation = !elevateStore.elevated; + + const handleConfirm = async () => { + await clientStore.elevate(clientId, durationSeconds); + if (clientId === currentUser.user.clientId) { + currentUser.tryAuthenticate(); + } + fClose(); + }; + + const handleClose = () => { + elevateStore.cleanupOidcElevate(); + fClose(); + }; + + return ( + + Elevate Client: {clientName} + + {needsElevation ? ( + + ) : ( + + Duration + + + )} + + + + {!needsElevation && ( + + )} + + + ); +}); + +export default ElevateClientDialog; diff --git a/ui/src/tests/client.test.ts b/ui/src/tests/client.test.ts index 3ae0466..55f940d 100644 --- a/ui/src/tests/client.test.ts +++ b/ui/src/tests/client.test.ts @@ -19,8 +19,10 @@ enum Col { Name = 1, Token = 2, LastSeen = 3, - Edit = 4, - Delete = 5, + ElevationEnds = 4, + Elevate = 5, + Edit = 6, + Delete = 7, } const waitForClient = diff --git a/ui/src/types.ts b/ui/src/types.ts index 4f33949..0ab2849 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -15,6 +15,7 @@ export interface IClient { token: string; name: string; lastUsed: string | null; + elevatedUntil?: string; } export interface IPlugin {