This commit is contained in:
seb
2026-10-13 00:33:38 +02:00
parent 1ad9b14aa6
commit 4c5ccf7f64
12 changed files with 1152 additions and 645 deletions

View File

@@ -0,0 +1,312 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import RefreshIcon from '@mui/icons-material/Refresh';
import SecurityIcon from '@mui/icons-material/Security';
import ComputerIcon from '@mui/icons-material/Computer';
import KeyIcon from '@mui/icons-material/Key';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlined';
import { enqueueSnackbar } from 'notistack';
import { mcp } from '../../api';
import { notifyError } from '../../utils/notify';
import { SURFACE_BASE, SURFACE_RAISED, BORDER, DARK_2 } from '../../colors';
export default class McpAuthorizationsPanel extends Component {
constructor(props) {
super(props);
this.state = {
status: 'loading',
authorizations: [],
error: null,
confirmTarget: null,
revoking: false,
};
this.loadAuthorizations = this.loadAuthorizations.bind(this);
this.handleRevokeClick = this.handleRevokeClick.bind(this);
this.handleConfirmRevoke = this.handleConfirmRevoke.bind(this);
this.handleCloseConfirm = this.handleCloseConfirm.bind(this);
}
componentDidMount() {
void this.loadAuthorizations();
}
// #region loadAuthorizations
// MARK: calls GET /api/mcp/authorizations from ./api
async loadAuthorizations() {
this.setState({ status: 'loading', error: null });
try {
const res = await mcp.listAuthorizations();
this.setState({
authorizations: res?.authorizations || [],
status: 'ready',
});
} catch (err) {
this.setState({
status: 'error',
error: err?.message || 'Fehler beim Laden der MCP-Autorisierungen',
});
notifyError(err);
}
}
// #endregion
handleRevokeClick(client) {
this.setState({ confirmTarget: client });
}
handleCloseConfirm() {
if (this.state.revoking) return;
this.setState({ confirmTarget: null });
}
// #region handleConfirmRevoke
// MARK: calls DELETE /api/mcp/authorizations/:clientId from ./api
async handleConfirmRevoke() {
const { confirmTarget } = this.state;
if (!confirmTarget) return;
this.setState({ revoking: true });
try {
await mcp.revokeAuthorization(confirmTarget.clientId);
enqueueSnackbar(`Autorisierung für "${confirmTarget.clientName}" erfolgreich widerrufen.`, {
variant: 'success',
});
this.setState({ confirmTarget: null, revoking: false });
await this.loadAuthorizations();
} catch (err) {
this.setState({ revoking: false });
notifyError(err);
}
}
// #endregion
render() {
const { status, authorizations, error, confirmTarget, revoking } = this.state;
return (
<Box sx={{ maxWidth: 860 }}>
{/* Header */}
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" sx={{ mb: 2.5 }}>
<Box>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 0.5 }}>
<SecurityIcon sx={{ color: 'primary.main', fontSize: 24 }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
MCP-Autorisierungen & Sicherheit
</Typography>
</Stack>
<Typography variant="body2" sx={{ color: DARK_2 }}>
Verwalten Sie externe MCP-Clients (z. B. Visual Studio Code, Cursor), die Zugriff auf Dashback Tools haben.
</Typography>
</Box>
<IconButton
size="small"
onClick={this.loadAuthorizations}
disabled={status === 'loading'}
aria-label="Aktualisieren"
>
<RefreshIcon fontSize="small" />
</IconButton>
</Stack>
{error && (
<Alert severity="error" sx={{ mb: 2 }} action={
<Button color="inherit" size="small" onClick={this.loadAuthorizations}>
Wiederholen
</Button>
}>
{error}
</Alert>
)}
{status === 'loading' && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress size={36} />
</Box>
)}
{status === 'ready' && authorizations.length === 0 && (
<Card
variant="outlined"
sx={{
p: 4,
textAlign: 'center',
bgcolor: SURFACE_BASE,
borderColor: BORDER,
borderRadius: 2,
}}
>
<ComputerIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 1, opacity: 0.6 }} />
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}>
Keine aktiven MCP-Clients
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', maxWidth: 440, mx: 'auto' }}>
Derzeit sind keine externen Anwendungen autorisiert. Wenn Sie einen MCP-Client wie VS Code mit Dash verbinden, wird er hier aufgeführt.
</Typography>
</Card>
)}
{status === 'ready' && authorizations.length > 0 && (
<Stack spacing={2}>
{authorizations.map((auth) => {
const authorizedDate = auth.authorizedAt
? new Date(auth.authorizedAt).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
: '–';
return (
<Card
key={auth.clientId || auth.clientDbId}
variant="outlined"
sx={{
borderRadius: 2,
borderColor: BORDER,
bgcolor: 'background.paper',
transition: 'box-shadow 0.2s ease',
'&:hover': {
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
},
}}
>
<CardContent sx={{ p: 2.5, '&:last-child': { pb: 2.5 } }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent="space-between"
spacing={2}
>
<Stack direction="row" spacing={2} alignItems="center">
<Box
sx={{
width: 44,
height: 44,
borderRadius: '10px',
bgcolor: SURFACE_RAISED,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'primary.main',
flexShrink: 0,
}}
>
<ComputerIcon />
</Box>
<Box>
<Stack direction="row" alignItems="center" spacing={1} flexWrap="wrap">
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{auth.clientName || 'Unbenannter Client'}
</Typography>
{auth.scope && (
<Chip
size="small"
label={auth.scope}
variant="outlined"
sx={{
height: 20,
fontSize: '0.72rem',
borderColor: 'primary.light',
color: 'primary.main',
}}
/>
)}
</Stack>
<Typography
variant="caption"
sx={{
color: 'text.secondary',
fontFamily: 'monospace',
display: 'block',
mt: 0.25,
}}
>
Client ID: {auth.clientId}
</Typography>
<Stack direction="row" spacing={2} sx={{ mt: 0.75 }}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Autorisiert: <strong>{authorizedDate}</strong>
</Typography>
{auth.tokenCount != null && (
<Stack direction="row" spacing={0.5} alignItems="center">
<KeyIcon sx={{ fontSize: 13, color: 'text.secondary' }} />
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Aktive Tokens: <strong>{auth.tokenCount}</strong>
</Typography>
</Stack>
)}
</Stack>
</Box>
</Stack>
<Button
variant="outlined"
color="error"
size="small"
startIcon={<DeleteOutlineIcon />}
onClick={() => this.handleRevokeClick(auth)}
sx={{ flexShrink: 0, textTransform: 'none' }}
>
Widerrufen
</Button>
</Stack>
</CardContent>
</Card>
);
})}
</Stack>
)}
{/* Confirmation Dialog */}
<Dialog
open={Boolean(confirmTarget)}
onClose={this.handleCloseConfirm}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ fontWeight: 600 }}>
Autorisierung widerrufen?
</DialogTitle>
<DialogContent>
<DialogContentText>
Möchten Sie den Zugriff für <strong>{confirmTarget?.clientName}</strong> wirklich widerrufen?
Alle bestehenden Sitzungen und Tokens für diesen MCP-Client werden sofort ungültig.
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={this.handleCloseConfirm} disabled={revoking}>
Abbrechen
</Button>
<Button
onClick={this.handleConfirmRevoke}
color="error"
variant="contained"
disabled={revoking}
startIcon={revoking ? <CircularProgress size={16} color="inherit" /> : <DeleteOutlineIcon />}
>
{revoking ? 'Widerrufe...' : 'Ja, widerrufen'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
}

View File

@@ -19,6 +19,8 @@ import PrinterSettingsPanel from './PrinterSettingsPanel';
import SettingsKeysPanel from './SettingsKeysPanel';
import ImportExportSettingsPanel from './ImportExportSettingsPanel';
import PosTerminalSettingsPanel from './PosTerminalSettingsPanel';
import McpAuthorizationsPanel from './McpAuthorizationsPanel';
import SecurityIcon from '@mui/icons-material/Security';
import { SURFACE_BASE } from '../../colors';
const TAB_KEYS = {
@@ -28,6 +30,7 @@ const TAB_KEYS = {
printers: 'printers',
importExport: 'importExport',
posterminal: 'posterminal',
security: 'security',
};
const TABS = [
@@ -37,6 +40,7 @@ const TABS = [
{ id: TAB_KEYS.printers, label: 'Drucker', icon: <PrintOutlinedIcon />, Panel: PrinterSettingsPanel },
{ id: TAB_KEYS.importExport, label: 'Import/Export', icon: <ImportExportIcon />, Panel: ImportExportSettingsPanel },
{ id: TAB_KEYS.posterminal, label: 'POS Terminal', icon: <PointOfSaleIcon />, Panel: PosTerminalSettingsPanel },
{ id: TAB_KEYS.security, label: 'Sicherheit & MCP', icon: <SecurityIcon />, Panel: McpAuthorizationsPanel },
];
export default class SettingsDialog extends Component {