feat: Implement user and policy management UI

- Add comprehensive user management with CRUD operations
- Implement policy management with templates and custom JSON
- Create reusable components (ConfirmDialog, LoadingState, EmptyState)
- Add user creation with password validation and credential copying
- Implement policy creation with built-in templates
- Add policy attachment to users functionality
- Include search, filtering, and status management
- Add proper error handling and validation
- Implement Material-UI based responsive design

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
2025-07-23 16:45:39 +02:00
co-authored by Claude
parent a91d56751c
commit 55983d0088
13 changed files with 1818 additions and 16 deletions
@@ -0,0 +1,265 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
Alert,
IconButton,
InputAdornment,
List,
ListItem,
ListItemIcon,
ListItemText,
Tooltip,
} from '@mui/material';
import {
Visibility,
VisibilityOff,
CheckCircle,
Cancel,
ContentCopy,
} from '@mui/icons-material';
import userService from '../../services/userService';
interface CreateUserDialogProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
const CreateUserDialog: React.FC<CreateUserDialogProps> = ({
open,
onClose,
onSuccess,
}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [showSuccess, setShowSuccess] = useState(false);
const passwordValidation = userService.validatePassword(password);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!username.trim()) {
setError('Username is required');
return;
}
if (!passwordValidation.valid) {
setError('Please fix password validation errors');
return;
}
setLoading(true);
try {
await userService.createUser({
accessKey: username,
secretKey: password,
});
setShowSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create user');
setLoading(false);
}
};
const handleCopyCredentials = () => {
const credentials = `Username: ${username}\nPassword: ${password}`;
navigator.clipboard.writeText(credentials);
};
const handleClose = () => {
if (!showSuccess) {
onClose();
resetForm();
}
};
const handleSuccessClose = () => {
onSuccess();
onClose();
resetForm();
};
const resetForm = () => {
setUsername('');
setPassword('');
setShowPassword(false);
setError('');
setShowSuccess(false);
setLoading(false);
};
const getValidationIcon = (isValid: boolean) => {
return isValid ? (
<CheckCircle color="success" fontSize="small" />
) : (
<Cancel color="error" fontSize="small" />
);
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<form onSubmit={handleSubmit}>
<DialogTitle>
{showSuccess ? 'User Created Successfully' : 'Create New User'}
</DialogTitle>
<DialogContent>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{!showSuccess ? (
<>
<TextField
autoFocus
margin="dense"
label="Username"
fullWidth
value={username}
onChange={(e) => setUsername(e.target.value)}
required
disabled={loading}
sx={{ mb: 2 }}
/>
<TextField
margin="dense"
label="Password"
type={showPassword ? 'text' : 'password'}
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
{password && (
<Box mt={2}>
<Typography variant="subtitle2" gutterBottom>
Password Requirements:
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
{getValidationIcon(password.length >= 8)}
</ListItemIcon>
<ListItemText
primary="At least 8 characters"
primaryTypographyProps={{
color: password.length >= 8 ? 'text.primary' : 'text.secondary',
}}
/>
</ListItem>
<ListItem>
<ListItemIcon>
{getValidationIcon(/[A-Z]/.test(password))}
</ListItemIcon>
<ListItemText
primary="At least one uppercase letter"
primaryTypographyProps={{
color: /[A-Z]/.test(password) ? 'text.primary' : 'text.secondary',
}}
/>
</ListItem>
<ListItem>
<ListItemIcon>
{getValidationIcon(/[a-z]/.test(password))}
</ListItemIcon>
<ListItemText
primary="At least one lowercase letter"
primaryTypographyProps={{
color: /[a-z]/.test(password) ? 'text.primary' : 'text.secondary',
}}
/>
</ListItem>
<ListItem>
<ListItemIcon>
{getValidationIcon(/[0-9]/.test(password))}
</ListItemIcon>
<ListItemText
primary="At least one number"
primaryTypographyProps={{
color: /[0-9]/.test(password) ? 'text.primary' : 'text.secondary',
}}
/>
</ListItem>
</List>
</Box>
)}
</>
) : (
<Box>
<Alert severity="success" sx={{ mb: 2 }}>
User created successfully! Save these credentials securely - the password cannot be retrieved later.
</Alert>
<Box sx={{ bgcolor: 'grey.100', p: 2, borderRadius: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
<strong>Username:</strong> {username}
<br />
<strong>Password:</strong> {password}
</Typography>
</Box>
<Tooltip title="Copy credentials to clipboard">
<Button
variant="outlined"
startIcon={<ContentCopy />}
onClick={handleCopyCredentials}
fullWidth
>
Copy Credentials
</Button>
</Tooltip>
</Box>
)}
</DialogContent>
<DialogActions>
{!showSuccess ? (
<>
<Button onClick={handleClose} disabled={loading}>
Cancel
</Button>
<Button
type="submit"
variant="contained"
disabled={loading || !passwordValidation.valid}
>
Create User
</Button>
</>
) : (
<Button onClick={handleSuccessClose} variant="contained">
Close
</Button>
)}
</DialogActions>
</form>
</Dialog>
);
};
export default CreateUserDialog;
+259 -8
View File
@@ -1,15 +1,266 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
IconButton,
Button,
Typography,
Chip,
TextField,
InputAdornment,
Switch,
Tooltip,
Alert,
Snackbar,
} from '@mui/material';
import {
Delete,
Search,
PersonAdd,
Person,
} from '@mui/icons-material';
import LoadingState from '../shared/LoadingState';
import EmptyState from '../shared/EmptyState';
import ConfirmDialog from '../shared/ConfirmDialog';
import CreateUserDialog from './CreateUserDialog';
import userService, { User } from '../../services/userService';
const Users: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<string | null>(null);
const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' as 'success' | 'error' });
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
setLoading(true);
const userList = await userService.listUsers();
setUsers(userList);
} catch (error) {
showSnackbar(error instanceof Error ? error.message : 'Failed to load users', 'error');
} finally {
setLoading(false);
}
};
const handleCreateUser = () => {
setCreateDialogOpen(true);
};
const handleDeleteUser = (accessKey: string) => {
setUserToDelete(accessKey);
setDeleteDialogOpen(true);
};
const confirmDeleteUser = async () => {
if (!userToDelete) return;
try {
await userService.deleteUser(userToDelete);
showSnackbar('User deleted successfully', 'success');
await loadUsers();
} catch (error) {
showSnackbar(error instanceof Error ? error.message : 'Failed to delete user', 'error');
} finally {
setDeleteDialogOpen(false);
setUserToDelete(null);
}
};
const handleToggleUserStatus = async (accessKey: string, currentStatus: string) => {
try {
const newStatus = currentStatus === 'enabled' ? 'disabled' : 'enabled';
await userService.updateUserStatus(accessKey, newStatus);
showSnackbar(`User ${newStatus} successfully`, 'success');
await loadUsers();
} catch (error) {
showSnackbar(error instanceof Error ? error.message : 'Failed to update user status', 'error');
}
};
const showSnackbar = (message: string, severity: 'success' | 'error') => {
setSnackbar({ open: true, message, severity });
};
const filteredUsers = users.filter((user) =>
user.accessKey.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) {
return <LoadingState message="Loading users..." />;
}
return (
<Box>
<Typography variant="h4" gutterBottom>
Users
</Typography>
<Typography variant="body1" color="text.secondary">
User management functionality coming soon...
</Typography>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4">User Management</Typography>
<Button
variant="contained"
startIcon={<PersonAdd />}
onClick={handleCreateUser}
>
Create User
</Button>
</Box>
<Paper>
<Box p={2}>
<TextField
fullWidth
variant="outlined"
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
/>
</Box>
{filteredUsers.length === 0 ? (
<EmptyState
icon={<Person />}
title={searchTerm ? 'No users found' : 'No users yet'}
message={
searchTerm
? 'Try adjusting your search criteria'
: 'Create your first user to get started'
}
actionLabel={!searchTerm ? 'Create User' : undefined}
onAction={!searchTerm ? handleCreateUser : undefined}
/>
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
<TableCell>Status</TableCell>
<TableCell>Policy</TableCell>
<TableCell>Groups</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredUsers.map((user) => (
<TableRow key={user.accessKey}>
<TableCell>
<Box display="flex" alignItems="center" gap={1}>
<Person fontSize="small" color="action" />
<Typography variant="body2">{user.accessKey}</Typography>
</Box>
</TableCell>
<TableCell>
<Chip
label={user.status}
color={user.status === 'enabled' ? 'success' : 'default'}
size="small"
/>
</TableCell>
<TableCell>
{user.policyName ? (
<Chip label={user.policyName} size="small" variant="outlined" />
) : (
<Typography variant="body2" color="text.secondary">
No policy
</Typography>
)}
</TableCell>
<TableCell>
{user.memberOf && user.memberOf.length > 0 ? (
user.memberOf.map((group) => (
<Chip
key={group}
label={group}
size="small"
variant="outlined"
sx={{ mr: 0.5 }}
/>
))
) : (
<Typography variant="body2" color="text.secondary">
No groups
</Typography>
)}
</TableCell>
<TableCell align="right">
<Tooltip title={user.status === 'enabled' ? 'Disable user' : 'Enable user'}>
<Switch
checked={user.status === 'enabled'}
onChange={() => handleToggleUserStatus(user.accessKey, user.status)}
color="primary"
/>
</Tooltip>
<Tooltip title="Delete user">
<IconButton
onClick={() => handleDeleteUser(user.accessKey)}
color="error"
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
<CreateUserDialog
open={createDialogOpen}
onClose={() => setCreateDialogOpen(false)}
onSuccess={() => {
setCreateDialogOpen(false);
loadUsers();
showSnackbar('User created successfully', 'success');
}}
/>
<ConfirmDialog
open={deleteDialogOpen}
title="Delete User"
message={`Are you sure you want to delete user "${userToDelete}"? This action cannot be undone.`}
confirmText="Delete"
confirmColor="error"
onConfirm={confirmDeleteUser}
onCancel={() => {
setDeleteDialogOpen(false);
setUserToDelete(null);
}}
/>
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
>
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: '100%' }}
>
{snackbar.message}
</Alert>
</Snackbar>
</Box>
);
};
+1
View File
@@ -0,0 +1 @@
export { default } from './Users';