u
This commit is contained in:
312
src/components/settings/McpAuthorizationsPanel.jsx
Normal file
312
src/components/settings/McpAuthorizationsPanel.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user