Files
Redflag/docs/3_BACKLOG/P3-004_Token-Management-UI-Enhancement.md

13 KiB

Token Management UI Enhancement

Priority: P3 (Enhancement) Source Reference: From needs.md line 137 Status: Ready for Implementation

Problem Statement

Administrators can create and view registration tokens but cannot delete used or expired tokens from the web interface. Token cleanup requires manual database operations or calling cleanup endpoints, creating operational friction and making token housekeeping difficult.

Feature Description

Enhance the Token Management UI to include deletion functionality for registration tokens, allowing administrators to clean up used, expired, or revoked tokens directly from the web interface with proper confirmation dialogs and bulk operations.

Acceptance Criteria

  1. Delete button for individual tokens in Token Management page
  2. Confirmation dialog before token deletion
  3. Bulk deletion capability with checkbox selection
  4. Visual indication of token status (active, used, expired, revoked)
  5. Filter tokens by status for easier cleanup
  6. Audit trail for token deletions
  7. Safe deletion prevention for tokens with active agent dependencies
  8. Success/error feedback for deletion operations

Technical Approach

1. Backend API Enhancement

Token Deletion Endpoint (aggregator-server/internal/api/handlers/token_management.go):

// DELETE /api/v1/tokens/:id
func (h *TokenHandler) DeleteToken(c *gin.Context) {
    tokenID, err := uuid.Parse(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid token ID"})
        return
    }

    // Check if token has active agents
    activeAgents, err := h.tokenQueries.GetActiveAgentCount(tokenID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    if activeAgents > 0 {
        c.JSON(http.StatusConflict, gin.H{
            "error": "Cannot delete token with active agents",
            "active_agents": activeAgents,
        })
        return
    }

    // Delete token and related usage records
    err = h.tokenQueries.DeleteToken(tokenID)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // Log deletion for audit trail
    log.Printf("Token %s deleted by user %s", tokenID, getUserID(c))

    c.JSON(http.StatusOK, gin.H{"message": "Token deleted successfully"})
}

Database Operations (aggregator-server/internal/database/queries/registration_tokens.go):

-- Check for active agents using token
SELECT COUNT(*) FROM agents
WHERE registration_token_id = $1
  AND last_seen > NOW() - INTERVAL '24 hours';

-- Delete token and usage records
DELETE FROM registration_token_usage WHERE token_id = $1;
DELETE FROM registration_tokens WHERE id = $1;

2. Frontend Token Management Enhancement

Enhanced Token Table (aggregator-web/src/pages/TokenManagement.tsx):

interface Token {
    id: string;
    token: string;
    max_seats: number;
    seats_used: number;
    status: 'active' | 'used' | 'expired' | 'revoked';
    created_at: string;
    expires_at: string;
    active_agents: number;
}

const TokenManagement: React.FC = () => {
    const [tokens, setTokens] = useState<Token[]>([]);
    const [selectedTokens, setSelectedTokens] = useState<string[]>([]);
    const [filter, setFilter] = useState<string>('all');

    const handleDeleteToken = async (tokenId: string) => {
        if (!window.confirm('Are you sure you want to delete this token? This action cannot be undone.')) {
            return;
        }

        try {
            await api.delete(`/tokens/${tokenId}`);
            setTokens(tokens.filter(token => token.id !== tokenId));
            showToast('Token deleted successfully', 'success');
        } catch (error) {
            showToast(error.message, 'error');
        }
    };

    const handleBulkDelete = async () => {
        if (selectedTokens.length === 0) {
            showToast('No tokens selected', 'warning');
            return;
        }

        if (!window.confirm(`Are you sure you want to delete ${selectedTokens.length} token(s)?`)) {
            return;
        }

        try {
            await Promise.all(selectedTokens.map(tokenId => api.delete(`/tokens/${tokenId}`)));
            setTokens(tokens.filter(token => !selectedTokens.includes(token.id)));
            setSelectedTokens([]);
            showToast(`${selectedTokens.length} token(s) deleted successfully`, 'success');
        } catch (error) {
            showToast('Some tokens could not be deleted', 'error');
        }
    };

    return (
        <div className="token-management">
            <div className="token-header">
                <h2>Registration Tokens</h2>
                <div className="token-actions">
                    <TokenFilter value={filter} onChange={setFilter} />
                    <BulkDeleteButton
                        selectedCount={selectedTokens.length}
                        onDelete={handleBulkDelete}
                    />
                </div>
            </div>

            <div className="token-table">
                <table>
                    <thead>
                        <tr>
                            <th>
                                <input
                                    type="checkbox"
                                    onChange={(e) => {
                                        if (e.target.checked) {
                                            setSelectedTokens(tokens.map(t => t.id));
                                        } else {
                                            setSelectedTokens([]);
                                        }
                                    }}
                                />
                            </th>
                            <th>Token</th>
                            <th>Status</th>
                            <th>Seats</th>
                            <th>Active Agents</th>
                            <th>Created</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        {tokens.map(token => (
                            <tr key={token.id}>
                                <td>
                                    <input
                                        type="checkbox"
                                        checked={selectedTokens.includes(token.id)}
                                        onChange={(e) => {
                                            if (e.target.checked) {
                                                setSelectedTokens([...selectedTokens, token.id]);
                                            } else {
                                                setSelectedTokens(selectedTokens.filter(id => id !== token.id));
                                            }
                                        }}
                                    />
                                </td>
                                <td>
                                    <code>{token.token.substring(0, 20)}...</code>
                                </td>
                                <td>
                                    <TokenStatusBadge status={token.status} />
                                </td>
                                <td>{token.seats_used}/{token.max_seats}</td>
                                <td>{token.active_agents}</td>
                                <td>{formatDate(token.created_at)}</td>
                                <td>
                                    <div className="token-actions">
                                        <CopyTokenButton token={token.token} />
                                        {token.status !== 'active' && token.active_agents === 0 && (
                                            <DeleteButton
                                                onDelete={() => handleDeleteToken(token.id)}
                                                disabled={token.active_agents > 0}
                                            />
                                        )}
                                    </div>
                                </td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>
        </div>
    );
};

3. Token Status Management

Token Status Calculation:

func (s *TokenService) GetTokenStatus(token RegistrationToken) string {
    now := time.Now()

    if token.ExpiresAt.Before(now) {
        return "expired"
    }

    if token.Status == "revoked" {
        return "revoked"
    }

    if token.SeatsUsed >= token.MaxSeats {
        return "used"
    }

    return "active"
}

Status Badge Component:

const TokenStatusBadge: React.FC<{ status: string }> = ({ status }) => {
    const getStatusColor = (status: string) => {
        switch (status) {
            case 'active': return 'green';
            case 'used': return 'blue';
            case 'expired': return 'red';
            case 'revoked': return 'orange';
            default: return 'gray';
        }
    };

    return (
        <span className={`token-status-badge ${getStatusColor(status)}`}>
            {status.charAt(0).toUpperCase() + status.slice(1)}
        </span>
    );
};

4. Safety Measures

Active Agent Check:

  • Prevent deletion of tokens with agents that checked in within last 24 hours
  • Show warning with number of active agents
  • Require explicit confirmation for tokens with active agents

Audit Logging:

type TokenAuditLog struct {
    TokenID   uuid.UUID `json:"token_id"`
    Action    string    `json:"action"`
    UserID    string    `json:"user_id"`
    Timestamp time.Time `json:"timestamp"`
    IPAddress string    `json:"ip_address"`
}

Definition of Done

  • Token deletion API endpoint implemented with safety checks
  • Individual token deletion in UI with confirmation dialog
  • Bulk deletion functionality with checkbox selection
  • Token status filtering and visual indicators
  • Active agent dependency checking
  • Audit trail for token operations
  • Error handling and user feedback
  • Responsive design for mobile viewing

Test Plan

  1. Unit Tests

    • Token deletion safety checks
    • Active agent count queries
    • Status calculation logic
  2. Integration Tests

    • End-to-end token deletion flow
    • Bulk deletion operations
    • Error handling scenarios
  3. Safety Tests

    • Attempt to delete token with active agents
    • Token status calculations for edge cases
    • Audit trail verification
  4. User Acceptance Tests

    • Administrators can easily identify and delete unused tokens
    • Safety mechanisms prevent accidental deletion of active tokens
    • Clear feedback and confirmation for all operations

Files to Modify

  • aggregator-server/internal/api/handlers/token_management.go - Add deletion endpoint
  • aggregator-server/internal/database/queries/registration_tokens.go - Add deletion queries
  • aggregator-web/src/pages/TokenManagement.tsx - Add deletion UI
  • aggregator-web/src/components/TokenStatusBadge.tsx - Status indicator component
  • aggregator-web/src/lib/api.ts - API integration

Token Deletion Rules

Safe to Delete

  • Tokens with status 'expired'
  • Tokens with status 'used' and 0 active agents
  • Tokens with status 'revoked' and 0 active agents

Not Safe to Delete

  • Tokens with status 'active'
  • Tokens with any active agents (checked in within 24 hours)
  • Tokens with pending agent registrations

User Experience

  1. Warning Messages: Clear indication of why token cannot be deleted
  2. Active Agent Count: Show number of agents using the token
  3. Confirmation Dialog: Explicit confirmation before deletion
  4. Success Feedback: Clear confirmation of successful deletion

Estimated Effort

  • Development: 10-12 hours
  • Testing: 6-8 hours
  • Review: 3-4 hours

Dependencies

  • Existing token management system
  • Agent registration and tracking
  • User authentication and authorization

Risk Assessment

Low Risk - Enhancement that adds new functionality without affecting existing systems. Safety checks prevent accidental deletion of critical tokens.

Implementation Phases

Phase 1: Backend API

  1. Implement token deletion safety checks
  2. Create deletion API endpoint
  3. Add audit logging

Phase 2: Frontend UI

  1. Add delete buttons and confirmation dialogs
  2. Implement bulk selection and deletion
  3. Add status filtering

Phase 3: Polish

  1. Error handling and user feedback
  2. Mobile responsiveness
  3. Accessibility improvements

Future Enhancements

  1. Token Expiration Automation: Automatic cleanup of expired tokens
  2. Token Templates: Pre-configured token settings for common use cases
  3. Token Usage Analytics: Detailed analytics on token usage patterns
  4. Token Import/Export: Bulk token management capabilities
  5. Token Permissions: Role-based access to token management features