import {useEffect, useState} from 'react'; import {Header} from '@/components/layout/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, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; import {Card, CardContent, CardDescription, 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 {Copy, Edit, Key, Loader2, MoreVertical, Plus, Search, ShieldCheck, ShieldX, Trash2,} from 'lucide-react'; import {toast} from 'sonner'; export function AccessControl() { const [keys, setKeys] = useState([]); const [filteredKeys, setFilteredKeys] = 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); // 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); 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); useEffect(() => { const fetchKeys = async () => { try { setIsLoading(true); const data = await accessApi.listKeys(); setKeys(data); setFilteredKeys(data); } catch (error) { console.error('Failed to fetch keys:', error); } finally { setIsLoading(false); } }; fetchKeys(); }, []); useEffect(() => { const filtered = keys.filter( (key) => key.name.toLowerCase().includes(searchQuery.toLowerCase()) || key.accessKeyId.toLowerCase().includes(searchQuery.toLowerCase()) ); setFilteredKeys(filtered); }, [searchQuery, keys]); 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; } 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); } }; // 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'; }; return (
API Keys Bucket Policies {/* Stats */}
Total Keys
{keys.length}
Active Keys
{keys.filter((k) => k.status === 'active').length}
Inactive Keys
{keys.filter((k) => k.status === 'inactive').length}
{/* Toolbar */}
setSearchQuery(e.target.value)} className="pl-8" />
{/* Keys Table */}
Name Access Key ID Status Created Last Used Permissions {isLoading ? (
Loading API keys...
) : filteredKeys.length === 0 ? ( {searchQuery ? 'No keys found matching your search' : 'No API keys yet'} ) : ( filteredKeys.map((key) => ( {key.name}
{key.accessKeyId}
{key.status} {formatDate(key.createdAt)} {key.lastUsed ? formatDate(key.lastUsed) : 'Never'}
{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 )}
handleRevealSecretKey(key)}> View Secret Key handleOpenEditPermissions(key)}> Edit Permissions handleOpenSettings(key)}> {key.status === 'active' ? ( <> Manage Status ) : ( <> Manage Status )} { setSelectedKey(key); setDeleteDialogOpen(true); }} > Delete
)) )}
Bucket Policies Manage resource-based policies for your buckets

Bucket policy editor coming soon...

{/* Create Key Dialog */} {newlyCreatedKey ? ( // Success state - show the secret key <> API Key Created Successfully Save your secret access key now. You won't be able to see it again.
{newlyCreatedKey.name}
{newlyCreatedKey.accessKeyId}
{newlyCreatedKey.secretKey}

Important: Save This Key Now

This is the only time you'll see the secret access key. Make sure to copy and save it securely. If you lose it, you'll need to create a new key.

) : ( // Creation form <> Create API Key Create a new API key with optional bucket permissions
setNewKeyName(e.target.value)} />

A friendly name to identify this API key

{/* Optional: Grant permissions during creation */}

You can also grant permissions later from the Edit Permissions menu

{createGrantPermissions && (
{/* Bucket Selection */}
{/* Permissions */} {createSelectedBucket && (
)}
)}
)}
{/* Delete Key Dialog */} Delete API Key Are you sure you want to delete "{selectedKey?.name}"? Applications using this key will lose access immediately. {/* Secret Key Dialog */} Secret Access Key Copy your secret access key now. For security reasons, it cannot be viewed again.
{selectedKey?.name}
{selectedKey?.accessKeyId}
{isLoadingSecretKey ? (
Loading secret key...
) : ( <> {revealedSecretKey} )}

Security Warning

Keep this secret key secure. Anyone with access to it can perform operations on your behalf.

{/* Key Settings Dialog */} Key Settings - {settingsKey?.name} Manage activation status and expiration date for this API key
{/* Status */}

Inactive keys cannot be used for authentication

{/* Expiration */}
{!neverExpires && (
setExpirationDate(e.target.value)} className="w-full" />

Key will automatically become inactive after this date

)}
{/* Current Status Display */}
Current Status: {settingsKey?.status}
{settingsKey?.expiration && (
Current Expiration: {formatDate(settingsKey.expiration)}
)}
{/* Edit Permissions Dialog */} Edit Bucket Permissions - {editingKey?.name} Grant this access key permissions on buckets
{/* Bucket Selection */}

Choose which bucket this key should have permissions on. Current permissions will be displayed when selected.

{/* Permissions */}
setPermissionRead(checked as boolean)} />

Allows reading objects from the bucket (GetObject, HeadObject, ListObjects)

setPermissionWrite(checked as boolean)} />

Allows writing and deleting objects in the bucket (PutObject, DeleteObject)

setPermissionOwner(checked as boolean)} />

Allows managing bucket settings and policies (DeleteBucket, PutBucketPolicy)

{/* Current Permissions Info */} {selectedBucket && editingKey && (
{(() => { const bucketPermission = editingKey.permissions.find( perm => perm.bucketName === selectedBucket || perm.bucketId === selectedBucket ); if (bucketPermission) { const hasPermissions = bucketPermission.read || bucketPermission.write || bucketPermission.owner; if (hasPermissions) { return (

This key currently has the following permissions on this bucket:

{bucketPermission.read && ( Read )} {bucketPermission.write && ( Write )} {bucketPermission.owner && ( Owner )}

Modify the checkboxes above to update permissions

); } } return (

This key has no permissions on this bucket yet. Select permissions above to grant access.

); })()}
)} {/* Current Bucket Permissions List */} {editingKey && editingKey.permissions.length > 0 && (
{editingKey.permissions.map((perm, idx) => (
{perm.bucketName}
{perm.read && R} {perm.write && W} {perm.owner && O}
))}
)}
); }