Files
authsec_react_materail_ui/src/components/Dashboard/TokenRegistery/TokenRegistery.js
Gaurav Kumar 773b1f753b material
2025-06-13 10:04:33 +05:30

746 lines
25 KiB
JavaScript

import React, { useState, useEffect } from "react";
import {
Button,
Modal,
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Pagination,
IconButton,
Checkbox,
FormControlLabel,
Chip,
Box,
Typography,
Card,
CardHeader,
CardContent,
Divider,
Grid,
TextareaAutosize,
Popover,
List,
ListItem,
ListItemText,
ListItemIcon,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
DialogContentText,
CircularProgress
} from "@mui/material";
import {
Edit as EditIcon,
Delete as DeleteIcon,
Add as AddIcon,
Menu as MenuIcon,
Close as CloseIcon,
Search as SearchIcon,
LibraryBooks as LibraryBooksIcon,
CheckCircle as CheckCircleIcon
} from "@mui/icons-material";
import { toast } from "react-toastify";
import * as tokenRegistryAPI from './tokenregistryapi';
function TOKENRegistry() {
const [tokens, setTokens] = useState([]);
const [showAddEditModal, setShowAddEditModal] = useState(false);
const [showGenerateTokenModal, setShowGenerateTokenModal] = useState(false);
const [newTokenName, setNewTokenName] = useState("");
const [generatedToken, setGeneratedToken] = useState("");
const [currentToken, setCurrentToken] = useState({
id: "",
tokenName: "",
tokenValue: "",
isActive: true,
scopes: []
});
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const [isEditing, setIsEditing] = useState(false);
const [recordsPerPage, setRecordsPerPage] = useState(10);
const [visibleColumns, setVisibleColumns] = useState({
id: true,
tokenName: true,
tokenValue: true,
scopes: true,
isActive: true,
actions: true
});
const [loading, setLoading] = useState(true);
const [selectedScope, setSelectedScope] = useState("");
const [selectedScopes, setSelectedScopes] = useState([]);
const [availableScopes] = useState([
{ value: 'read', label: 'Read Access' },
{ value: 'write', label: 'Write Access' },
{ value: 'delete', label: 'Delete Access' },
{ value: 'admin', label: 'Admin Access' },
]);
const [anchorEl, setAnchorEl] = useState(null);
const [columnsAnchorEl, setColumnsAnchorEl] = useState(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [tokenToDelete, setTokenToDelete] = useState(null);
useEffect(() => {
fetchTokens();
}, []);
const fetchTokens = async () => {
setLoading(true);
try {
const data = await tokenRegistryAPI.fetchAllTokens();
setTokens(data);
} catch (error) {
handleApiError(error, "fetch tokens");
} finally {
setLoading(false);
}
};
const handleApiError = (error, action) => {
if (error.message === 'Unauthorized') {
toast.error("Your session has expired. Please log in again.");
} else {
toast.error(`Failed to ${action}`);
console.error(`Error ${action}:`, error);
}
};
const toggleColumn = (column) => {
setVisibleColumns(prev => ({
...prev,
[column]: !prev[column],
}));
};
const handleInputChange = (event) => {
const { name, value, type, checked } = event.target;
setCurrentToken(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleSearch = (query) => {
setSearchQuery(query);
setCurrentPage(1);
};
const handleClose = () => {
setShowAddEditModal(false);
setShowGenerateTokenModal(false);
setGeneratedToken("");
setNewTokenName("");
setSelectedScopes([]);
};
const handleRecordsPerPageChange = (number) => {
setRecordsPerPage(number);
setCurrentPage(1);
setAnchorEl(null);
};
const filteredTokens = tokens.filter(
(item) =>
item.tokenName &&
item.tokenName.toLowerCase().includes(searchQuery.toLowerCase())
);
const totalPages = Math.ceil(filteredTokens.length / recordsPerPage);
const handlePageChange = (event, pageNumber) => {
setCurrentPage(pageNumber);
};
const slicedTokens = filteredTokens.slice(
(currentPage - 1) * recordsPerPage,
currentPage * recordsPerPage
);
const handleSubmit = async (event) => {
event.preventDefault();
try {
if (isEditing) {
await tokenRegistryAPI.updateToken(currentToken.id, currentToken);
toast.success("Token updated successfully!");
} else {
await tokenRegistryAPI.createToken(currentToken);
toast.success("Token added successfully!");
}
setShowAddEditModal(false);
fetchTokens();
} catch (error) {
handleApiError(error, isEditing ? "update token" : "add token");
}
};
const openModal = (token = { id: "", tokenName: "", tokenValue: "", isActive: false, scopes: [] }) => {
setIsEditing(!!token.id);
setCurrentToken(token);
setSelectedScopes(token.scopes || []);
setShowAddEditModal(true);
};
const handleDeleteClick = (id) => {
setTokenToDelete(id);
setDeleteConfirmOpen(true);
};
const handleDeleteConfirm = async () => {
try {
await tokenRegistryAPI.deleteToken(tokenToDelete);
toast.success('Token deleted successfully');
fetchTokens();
} catch (error) {
handleApiError(error, "delete token");
} finally {
setDeleteConfirmOpen(false);
setTokenToDelete(null);
}
};
const addScope = () => {
if (selectedScope && !selectedScopes.includes(selectedScope)) {
setSelectedScopes([...selectedScopes, selectedScope]);
setSelectedScope("");
}
};
const removeScope = (scopeToRemove) => {
setSelectedScopes(selectedScopes.filter(scope => scope !== scopeToRemove));
};
const generateNewToken = async () => {
if (!newTokenName.trim()) {
toast.error("Please enter a token name");
return;
}
try {
const data = await tokenRegistryAPI.generateToken({
name: newTokenName,
scopes: selectedScopes
});
setGeneratedToken(data.tokenValue);
toast.success("Token generated successfully!");
fetchTokens();
} catch (error) {
handleApiError(error, "generate token");
}
};
const handleMenuClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleColumnsMenuClick = (event) => {
setColumnsAnchorEl(event.currentTarget);
};
const handleColumnsMenuClose = () => {
setColumnsAnchorEl(null);
};
return (
<Box sx={{ marginTop: "1rem", padding: 2 }}>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress />
</Box>
) : (
<Box>
{/* Token Generation Card */}
<Card sx={{ mb: 4, boxShadow: 3 }}>
<CardHeader
title="Token Management"
action={
<Button
variant="contained"
onClick={() => setShowGenerateTokenModal(true)}
sx={{ backgroundColor: '#1976d2', color: 'white' }}
>
Generate New Token
</Button>
}
/>
<CardContent>
<Typography variant="body1">
Manage your API tokens here. Generate new tokens or delete existing ones.
</Typography>
</CardContent>
</Card>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={4}>
<Typography variant="h4" component="h1" sx={{ fontWeight: 'bold' }}>
Token Registry
</Typography>
</Box>
<Grid container spacing={2} alignItems="center" mb={3}>
<Grid item xs={12} md={8} lg={6}>
<TextField
fullWidth
variant="outlined"
placeholder="Search"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
InputProps={{
startAdornment: (
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
),
sx: {
borderRadius: '10px',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
}
}}
/>
</Grid>
<Grid item xs={12} md={4} lg={6} display="flex" justifyContent="flex-end">
<IconButton
onClick={() => openModal()}
color="primary"
sx={{ mr: 2 }}
>
<AddIcon />
</IconButton>
<IconButton
onClick={handleColumnsMenuClick}
color="primary"
>
<MenuIcon />
</IconButton>
</Grid>
</Grid>
<TableContainer component={Paper} sx={{ boxShadow: 3 }}>
<Table>
<TableHead sx={{ backgroundColor: '#f5f5f5' }}>
<TableRow>
{Object.keys(visibleColumns).filter(key => visibleColumns[key]).map(key => (
<TableCell key={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{slicedTokens.length === 0 ? (
<TableRow>
<TableCell
colSpan={Object.keys(visibleColumns).filter(key => visibleColumns[key]).length}
align="center"
>
No Data Available
</TableCell>
</TableRow>
) : (
slicedTokens.map((token, index) => (
<TableRow key={index} hover>
{Object.keys(visibleColumns).filter(key => visibleColumns[key]).map(key => (
<TableCell key={key}>
{key === "actions" ? (
<>
<IconButton
onClick={() => openModal(token)}
color="primary"
sx={{ mr: 1 }}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => handleDeleteClick(token.id)}
color="error"
>
<DeleteIcon />
</IconButton>
</>
) : key === "isActive" ? (
<Chip
label={token.isActive ? "Active" : "Inactive"}
color={token.isActive ? "success" : "error"}
size="small"
sx={{
backgroundColor: token.isActive ? '#d4f7d4' : '#f7d4d4',
color: token.isActive ? 'green' : 'red'
}}
/>
) : key === "scopes" ? (
<Box>
{token.scopes && token.scopes.map(scope => {
const scopeInfo = availableScopes.find(s => s.value === scope);
return (
<Chip
key={scope}
label={scopeInfo?.label || scope}
color="primary"
size="small"
sx={{ mr: 1, mb: 1 }}
/>
);
})}
</Box>
) : (
token[key]
)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{/* Manage Columns */}
<Popover
open={Boolean(columnsAnchorEl)}
anchorEl={columnsAnchorEl}
onClose={handleColumnsMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<List>
{Object.keys(visibleColumns).map((column) => (
<ListItem key={column} dense button onClick={() => toggleColumn(column)}>
<ListItemIcon>
<Checkbox
edge="start"
checked={visibleColumns[column]}
tabIndex={-1}
disableRipple
/>
</ListItemIcon>
<ListItemText primary={column.charAt(0).toUpperCase() + column.slice(1).toLowerCase()} />
</ListItem>
))}
</List>
</Popover>
{/* Records Per Page */}
<Box display="flex" justifyContent="flex-end" mt={2}>
<Button
variant="outlined"
startIcon={<LibraryBooksIcon />}
onClick={handleMenuClick}
sx={{ border: '2px solid', borderRadius: '8px', boxShadow: 1 }}
>
{recordsPerPage}
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={handleMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<List>
{[1, 5, 10, 20, 50].map((number) => (
<ListItem
key={number}
dense
button
onClick={() => handleRecordsPerPageChange(number)}
sx={{ minWidth: '100px' }}
>
<ListItemText primary={number} />
{recordsPerPage === number && <CheckCircleIcon color="primary" />}
</ListItem>
))}
</List>
</Popover>
</Box>
<Box display="flex" justifyContent="center" mt={2}>
<Pagination
count={totalPages}
page={currentPage}
onChange={handlePageChange}
color="primary"
showFirstButton
showLastButton
/>
</Box>
{/* Generate Token Modal */}
<Dialog open={showGenerateTokenModal} onClose={handleClose} fullWidth maxWidth="sm">
<DialogTitle>
Generate New Token
<IconButton
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box component="form" sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
label="Token Name"
value={newTokenName}
onChange={(e) => setNewTokenName(e.target.value)}
required
sx={{ mb: 2 }}
/>
<Typography variant="subtitle1" gutterBottom>
Token Scopes
</Typography>
<Box display="flex" alignItems="center" mb={2}>
<FormControl fullWidth sx={{ mr: 2 }}>
<InputLabel>Select a scope</InputLabel>
<Select
value={selectedScope}
onChange={(e) => setSelectedScope(e.target.value)}
label="Select a scope"
>
<MenuItem value=""><em>Select a scope</em></MenuItem>
{availableScopes.map(scope => (
<MenuItem key={scope.value} value={scope.value}>
{scope.label}
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="outlined"
onClick={addScope}
disabled={!selectedScope}
>
Add Scope
</Button>
</Box>
<Typography variant="caption" display="block" gutterBottom>
Select and add the permissions this token should have
</Typography>
{/* Display selected scopes as chips */}
{selectedScopes.length > 0 && (
<Box sx={{ mt: 2 }}>
{selectedScopes.map(scope => {
const scopeInfo = availableScopes.find(s => s.value === scope);
return (
<Chip
key={scope}
label={scopeInfo?.label || scope}
color="primary"
onDelete={() => removeScope(scope)}
sx={{ mr: 1, mb: 1 }}
/>
);
})}
</Box>
)}
{generatedToken && (
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1" gutterBottom>
Generated Token
</Typography>
<TextareaAutosize
minRows={3}
value={generatedToken}
readOnly
style={{ width: '100%', padding: '8px', borderRadius: '4px', borderColor: '#ccc' }}
/>
<Typography variant="caption" display="block" gutterBottom>
Copy this token and store it securely. You won't be able to see it again.
</Typography>
</Box>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
<Button
variant="contained"
onClick={generateNewToken}
disabled={!newTokenName.trim()}
>
Generate Token
</Button>
</DialogActions>
</Dialog>
{/* Add/Edit Token Modal */}
<Dialog open={showAddEditModal} onClose={handleClose} fullWidth maxWidth="sm">
<DialogTitle>
{isEditing ? "Edit Token" : "Add Token"}
<IconButton
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
label="Token Name"
name="tokenName"
value={currentToken.tokenName}
onChange={handleInputChange}
required
sx={{ mb: 2 }}
/>
<TextField
margin="normal"
fullWidth
label="Token Value"
name="tokenValue"
value={currentToken.tokenValue}
onChange={handleInputChange}
required
sx={{ mb: 2 }}
/>
<Typography variant="subtitle1" gutterBottom>
Token Scopes
</Typography>
<Box display="flex" alignItems="center" mb={2}>
<FormControl fullWidth sx={{ mr: 2 }}>
<InputLabel>Select a scope</InputLabel>
<Select
value={selectedScope}
onChange={(e) => setSelectedScope(e.target.value)}
label="Select a scope"
>
<MenuItem value=""><em>Select a scope</em></MenuItem>
{availableScopes.map(scope => (
<MenuItem key={scope.value} value={scope.value}>
{scope.label}
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="outlined"
onClick={() => {
if (selectedScope && !currentToken.scopes.includes(selectedScope)) {
setCurrentToken(prev => ({
...prev,
scopes: [...prev.scopes, selectedScope]
}));
setSelectedScope("");
}
}}
disabled={!selectedScope}
>
Add Scope
</Button>
</Box>
<Typography variant="caption" display="block" gutterBottom>
Select and add the permissions this token should have
</Typography>
{/* Display selected scopes as chips */}
{currentToken.scopes && currentToken.scopes.length > 0 && (
<Box sx={{ mt: 2 }}>
{currentToken.scopes.map(scope => {
const scopeInfo = availableScopes.find(s => s.value === scope);
return (
<Chip
key={scope}
label={scopeInfo?.label || scope}
color="primary"
onDelete={() => {
setCurrentToken(prev => ({
...prev,
scopes: prev.scopes.filter(s => s !== scope)
}));
}}
sx={{ mr: 1, mb: 1 }}
/>
);
})}
</Box>
)}
<FormControlLabel
control={
<Checkbox
checked={currentToken.isActive}
onChange={handleInputChange}
name="isActive"
color="primary"
/>
}
label="Active?"
sx={{ mt: 2 }}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
<Button
variant="contained"
onClick={handleSubmit}
type="submit"
>
{isEditing ? "Update Token" : "Add Token"}
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog
open={deleteConfirmOpen}
onClose={() => setDeleteConfirmOpen(false)}
>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete this token?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)}>Cancel</Button>
<Button onClick={handleDeleteConfirm} color="error" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
)}
</Box>
);
}
export default TOKENRegistry;