import {useEffect, useMemo, useState} from 'react';
import {cn} from '@/lib/utils';
import {PageHeader} from '@/components/ui/page-header';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Badge} from '@/components/ui/badge';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { IconTile } from '@/components/ui/icon-tile';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {Tabs, TabsContent} from '@/components/ui/tabs';
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import {Checkbox} from '@/components/ui/checkbox';
import {Select, SelectOption} from '@/components/ui/select';
import {accessApi, bucketsApi} from '@/lib/api';
import {formatDate} from '@/lib/utils';
import type {AccessKey, Bucket, BucketPermission} from '@/types';
import {AlertTriangle, Calendar, Check, Copy, Database, Edit, Eye, EyeOff, Key, KeyRound, Loader2, MoreVertical, Plus, Search, ShieldCheck, ShieldX, Trash2,} from 'lucide-react';
import {toast} from 'sonner';
function CredentialField({
label,
value,
mono = true,
breakAll = false,
maskable = false,
loading = false,
}: {
label: string;
value: string;
mono?: boolean;
breakAll?: boolean;
maskable?: boolean;
loading?: boolean;
}) {
const [copied, setCopied] = useState(false);
const [revealed, setRevealed] = useState(!maskable);
const copy = () => {
if (!value) return;
navigator.clipboard.writeText(value);
setCopied(true);
toast.success(`${label} copied`);
setTimeout(() => setCopied(false), 1600);
};
const display = loading ? '' : revealed || !maskable ? value : '•'.repeat(Math.min(40, value.length || 40));
return (
{maskable && (
)}
);
}
export function AccessControl() {
const [keys, setKeys] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedKey, setSelectedKey] = useState(null);
const [newKeyName, setNewKeyName] = useState('');
// Create key with permissions state
const [createAvailableBuckets, setCreateAvailableBuckets] = useState([]);
const [createSelectedBucket, setCreateSelectedBucket] = useState('');
const [createPermissionRead, setCreatePermissionRead] = useState(false);
const [createPermissionWrite, setCreatePermissionWrite] = useState(false);
const [createPermissionOwner, setCreatePermissionOwner] = useState(false);
const [createGrantPermissions, setCreateGrantPermissions] = useState(false);
const [newlyCreatedKey, setNewlyCreatedKey] = useState(null);
// Edit permissions state
const [editPermissionsDialogOpen, setEditPermissionsDialogOpen] = useState(false);
const [editingKey, setEditingKey] = useState(null);
const [availableBuckets, setAvailableBuckets] = useState([]);
const [selectedBucket, setSelectedBucket] = useState('');
const [permissionRead, setPermissionRead] = useState(false);
const [permissionWrite, setPermissionWrite] = useState(false);
const [permissionOwner, setPermissionOwner] = useState(false);
const [savingPermissions, setSavingPermissions] = useState(false);
// Key settings state (activation/expiration)
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [settingsKey, setSettingsKey] = useState(null);
const [keyStatus, setKeyStatus] = useState<'active' | 'inactive'>('active');
const [expirationDate, setExpirationDate] = useState('');
const [neverExpires, setNeverExpires] = useState(true);
// Secret key dialog state
const [secretKeyDialogOpen, setSecretKeyDialogOpen] = useState(false);
const [revealedSecretKey, setRevealedSecretKey] = useState('');
const [isLoadingSecretKey, setIsLoadingSecretKey] = useState(false);
// Key details dialog state
const [keyDetailsDialogOpen, setKeyDetailsDialogOpen] = useState(false);
const [viewingKey, setViewingKey] = useState(null);
const [detailsSecretKey, setDetailsSecretKey] = useState('');
const [isLoadingDetailsSecretKey, setIsLoadingDetailsSecretKey] = useState(false);
useEffect(() => {
const fetchKeys = async () => {
try {
setIsLoading(true);
const data = await accessApi.listKeys();
setKeys(data);
} catch (error) {
console.error('Failed to fetch keys:', error);
} finally {
setIsLoading(false);
}
};
fetchKeys();
}, []);
const filteredKeys = useMemo(
() =>
keys.filter(
(key) =>
key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
key.accessKeyId.toLowerCase().includes(searchQuery.toLowerCase())
),
[keys, searchQuery]
);
const handleCreateKey = async () => {
if (!newKeyName) {
toast.error('Please enter a key name');
return;
}
try {
const newKey = await accessApi.createKey(newKeyName);
// If user wants to grant permissions inline
if (createGrantPermissions && createSelectedBucket) {
if (createPermissionRead || createPermissionWrite || createPermissionOwner) {
try {
await bucketsApi.grantPermission(createSelectedBucket, newKey.accessKeyId, {
read: createPermissionRead,
write: createPermissionWrite,
owner: createPermissionOwner,
});
} catch (error) {
console.error('Failed to grant permissions:', error);
// Continue even if permission grant fails - key is already created
}
}
}
// Store the newly created key to show the secret key
setNewlyCreatedKey(newKey);
// Refresh keys list
const data = await accessApi.listKeys();
setKeys(data);
toast.success(`API Key "${newKeyName}" created successfully`);
} catch (error) {
// Error toast is handled by API interceptor
console.error('Create key error:', error);
}
};
const handleCloseCreateDialog = () => {
setCreateDialogOpen(false);
setNewKeyName('');
setCreateSelectedBucket('');
setCreatePermissionRead(false);
setCreatePermissionWrite(false);
setCreatePermissionOwner(false);
setCreateGrantPermissions(false);
setNewlyCreatedKey(null);
};
const handleOpenCreateDialog = async () => {
setCreateDialogOpen(true);
setNewKeyName('');
setCreateSelectedBucket('');
setCreatePermissionRead(false);
setCreatePermissionWrite(false);
setCreatePermissionOwner(false);
setCreateGrantPermissions(false);
// Load available buckets
try {
const buckets = await bucketsApi.list();
setCreateAvailableBuckets(buckets);
} catch (error) {
console.error('Failed to load buckets:', error);
}
};
const handleDeleteKey = async () => {
if (!selectedKey) return;
try {
await accessApi.deleteKey(selectedKey.accessKeyId);
const keyName = selectedKey.name;
setDeleteDialogOpen(false);
setSelectedKey(null);
// Refresh keys list
const data = await accessApi.listKeys();
setKeys(data);
toast.success(`API Key "${keyName}" deleted successfully`);
} catch (error) {
// Error toast is handled by API interceptor
console.error('Delete key error:', error);
}
};
const handleOpenSettings = (key: AccessKey) => {
setSettingsKey(key);
setKeyStatus(key.status);
setSettingsDialogOpen(true);
// Set expiration date if it exists
if (key.expiration) {
const expDate = new Date(key.expiration);
// Format as YYYY-MM-DDTHH:mm for datetime-local input
const formattedDate = expDate.toISOString().slice(0, 16);
setExpirationDate(formattedDate);
setNeverExpires(false);
} else {
setExpirationDate('');
setNeverExpires(true);
}
};
const handleSaveKeySettings = async () => {
if (!settingsKey) return;
try {
const updates: { status?: string; expiration?: string } = {};
updates.status = keyStatus;
if (!neverExpires && expirationDate) {
updates.expiration = new Date(expirationDate).toISOString();
} else if (neverExpires) {
// Clear expiration by setting status to active
updates.status = 'active';
}
await accessApi.updateKey(settingsKey.accessKeyId, updates);
// Refresh keys list
const data = await accessApi.listKeys();
setKeys(data);
setSettingsDialogOpen(false);
toast.success(`Key settings updated successfully`);
} catch (error) {
// Error toast is handled by API interceptor
console.error('Update key settings error:', error);
}
};
const handleRevealSecretKey = async (key: AccessKey) => {
setSelectedKey(key);
setIsLoadingSecretKey(true);
setSecretKeyDialogOpen(true);
setRevealedSecretKey('');
try {
const secretKey = await accessApi.getSecretKey(key.accessKeyId);
setRevealedSecretKey(secretKey);
} catch (error) {
console.error('Failed to fetch secret key:', error);
setSecretKeyDialogOpen(false);
} finally {
setIsLoadingSecretKey(false);
}
};
const handleOpenEditPermissions = async (key: AccessKey) => {
setEditingKey(key);
setEditPermissionsDialogOpen(true);
setSelectedBucket('');
setPermissionRead(false);
setPermissionWrite(false);
setPermissionOwner(false);
// Load available buckets
try {
const buckets = await bucketsApi.list();
setAvailableBuckets(buckets);
} catch (error) {
console.error('Failed to load buckets:', error);
}
};
const handleBucketChange = (bucketName: string) => {
setSelectedBucket(bucketName);
if (!bucketName || !editingKey) {
// Reset permissions if no bucket selected
setPermissionRead(false);
setPermissionWrite(false);
setPermissionOwner(false);
return;
}
// Find if this key already has permissions on the selected bucket
const bucketPermission = editingKey.permissions.find(
perm => perm.bucketName === bucketName || perm.bucketId === bucketName
);
if (bucketPermission) {
// Set the checkboxes to reflect current permissions
setPermissionRead(bucketPermission.read);
setPermissionWrite(bucketPermission.write);
setPermissionOwner(bucketPermission.owner);
} else {
// No permissions set yet, reset checkboxes
setPermissionRead(false);
setPermissionWrite(false);
setPermissionOwner(false);
}
};
const handleGrantBucketPermission = async () => {
if (!editingKey || !selectedBucket) {
toast.error('Please select a bucket');
return;
}
if (!permissionRead && !permissionWrite && !permissionOwner) {
toast.error('Please select at least one permission');
return;
}
setSavingPermissions(true);
try {
// Call backend API to grant bucket permissions
await bucketsApi.grantPermission(selectedBucket, editingKey.accessKeyId, {
read: permissionRead,
write: permissionWrite,
owner: permissionOwner,
});
toast.success(`Permissions granted on bucket "${selectedBucket}" successfully`);
setEditPermissionsDialogOpen(false);
setSelectedBucket('');
setPermissionRead(false);
setPermissionWrite(false);
setPermissionOwner(false);
// Refresh keys list to update permissions
const data = await accessApi.listKeys();
setKeys(data);
} catch (error) {
// Error toast is handled by API interceptor
console.error('Grant permission error:', error);
} finally {
setSavingPermissions(false);
}
};
// Helper function to format permission flags as a readable string
const formatPermissions = (perm: BucketPermission): string => {
const perms = [];
if (perm.read) perms.push('Read');
if (perm.write) perms.push('Write');
if (perm.owner) perms.push('Owner');
return perms.join(', ') || 'None';
};
const handleRowClick = async (key: AccessKey) => {
setViewingKey(key);
setKeyDetailsDialogOpen(true);
setDetailsSecretKey('');
setIsLoadingDetailsSecretKey(true);
// Fetch the secret key immediately
try {
const secretKey = await accessApi.getSecretKey(key.accessKeyId);
setDetailsSecretKey(secretKey);
} catch (error) {
console.error('Failed to fetch secret key:', error);
} finally {
setIsLoadingDetailsSecretKey(false);
}
};
return (
{/* Stats */}
Total Keys
{keys.length}
Active Keys
{keys.filter((k) => k.status === 'active').length}
Inactive Keys
{keys.filter((k) => k.status === 'inactive').length}
{/* Toolbar */}
{/* Keys Table */}
Name
Access Key ID
Status
Created
Permissions
{isLoading ? (
Loading API keys...
) : filteredKeys.length === 0 ? (
{searchQuery ? 'No keys found matching your search' : 'No API keys yet'}
) : (
filteredKeys.map((key) => (
handleRowClick(key)}
className="cursor-pointer hover:bg-muted/50"
>
{key.name}
{
e.stopPropagation();
navigator.clipboard.writeText(key.accessKeyId);
toast.success('Access Key ID copied to clipboard');
}}
>
{key.accessKeyId}
{key.status}
{formatDate(key.createdAt)}
{key.permissions.slice(0, 2).map((perm, idx) => (
{perm.bucketName}: {formatPermissions(perm)}
))}
{key.permissions.length > 2 && (
+{key.permissions.length - 2} more
)}
{key.permissions.length === 0 && (
No permissions
)}
e.stopPropagation()}>
handleRevealSecretKey(key)}>
View Secret Key
handleOpenEditPermissions(key)}>
Edit Permissions
handleOpenSettings(key)}>
{key.status === 'active' ? (
<>
Manage Status
>
) : (
<>
Manage Status
>
)}
{
setSelectedKey(key);
setDeleteDialogOpen(true);
}}
>
Delete
))
)}
{/* Create Key Dialog */}
{/* Delete Key Dialog */}
{/* Secret Key Dialog */}
{/* Key Settings Dialog */}
{/* Key Details Dialog */}
{/* Edit Permissions Dialog */}
);
}