From 809c336857b7b2e2e1fa6a4e2e92e5fce5eaf954 Mon Sep 17 00:00:00 2001 From: OmniZlatoon Date: Sun, 30 Aug 2026 14:12:06 +0100 Subject: [PATCH 1/4] fix(contracts): index reissued certificates and mark originals as superseded - Add new certificate to IssuerCertIds and OwnerCertIds indexes - Mark original certificate status as Revoked with reason 'Superseded' - Emit CertificateRevokedEvent for original certificate - Update test_reissue_certificate to verify all three requirements Fixes: reissue_certificate does not index the new certificate and leaves the old one active The fix ensures: 1. Reissued certificates are discoverable via get_certificates_by_issuer/owner 2. Original certificates are marked as revoked (not simultaneously active) 3. Proper audit trail with revocation events for both old and new certs --- stellar-contracts/src/lib.rs | 23 +++++++++++++++++++++-- stellar-contracts/src/test.rs | 15 +++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/stellar-contracts/src/lib.rs b/stellar-contracts/src/lib.rs index 07c1e0d5..37fd739b 100644 --- a/stellar-contracts/src/lib.rs +++ b/stellar-contracts/src/lib.rs @@ -488,15 +488,34 @@ impl CertificateContract { // Store new certificate Self::set_persistent(&env, &DataKey::Certificate(new_id.clone()), &new_cert); - // Emit issuance event + // Index the new certificate by issuer and owner + Self::append_cert_id(&env, DataKey::IssuerCertIds(issuer.clone()), new_id.clone()); + Self::append_cert_id(&env, DataKey::OwnerCertIds(new_cert.owner.clone()), new_id.clone()); + + // Mark original certificate as revoked (superseded) + let mut updated_original = original_cert; + updated_original.status = CertificateStatus::Revoked; + updated_original.revocation_reason = Some(String::from_str(&env, "Superseded")); + Self::set_persistent(&env, &DataKey::Certificate(old_id.clone()), &updated_original); + + // Emit issuance event for new certificate env.events().publish( (symbol_short!("issued"), new_id.clone()), CertificateIssuedEvent { id: new_id, - issuer, + issuer: issuer.clone(), owner: new_cert.owner, }, ); + + // Emit revocation event for original certificate + env.events().publish( + (symbol_short!("revoked"), old_id.clone()), + CertificateRevokedEvent { + id: old_id, + reason: String::from_str(&env, "Superseded"), + }, + ); } // --- Certificate Transfer Functions --- diff --git a/stellar-contracts/src/test.rs b/stellar-contracts/src/test.rs index af354bc3..1ec82562 100644 --- a/stellar-contracts/src/test.rs +++ b/stellar-contracts/src/test.rs @@ -150,10 +150,21 @@ fn test_reissue_certificate() { assert_eq!(new_cert.metadata_uri, new_metadata); assert_eq!(new_cert.parent_certificate_id, Some(old_id.clone())); - // Verify original certificate still exists + // Verify original certificate is now revoked (superseded) let original_cert = client.get_certificate(&old_id).expect("Certificate should exist"); assert_eq!(original_cert.id, old_id); - assert_eq!(original_cert.status, CertificateStatus::Active); // Original remains active + assert_eq!(original_cert.status, CertificateStatus::Revoked); + assert_eq!(original_cert.revocation_reason, Some(String::from_str(&env, "Superseded"))); + + // Verify new certificate is indexed by issuer + let issuer_certs = client.get_certificates_by_issuer(&issuer, &Pagination { page: 1, limit: 10 }); + let cert_ids: Vec = issuer_certs.data.iter().map(|c| c.id.clone()).collect(); + assert!(cert_ids.contains(&new_id), "New cert should be indexed by issuer"); + + // Verify new certificate is indexed by owner + let owner_certs = client.get_certificates_by_owner(&owner, &Pagination { page: 1, limit: 10 }); + let owner_cert_ids: Vec = owner_certs.data.iter().map(|c| c.id.clone()).collect(); + assert!(owner_cert_ids.contains(&new_id), "New cert should be indexed by owner"); } #[test] From 91d0c32619464568c5cac2e06d36f69292e9d767 Mon Sep 17 00:00:00 2001 From: OmniZlatoon Date: Sun, 30 Aug 2026 14:30:49 +0100 Subject: [PATCH 2/4] fix(frontend): block non-active certificates from revocation and fix status badge Create reusable StatusBadge component for certificate status display - Add isRevocableStatus() utility to validate active certificates only - Add getRevocationBlockedMessage() for user-friendly blocking reasons - Support statuses: active, revoked, expired, frozen, suspended - Include icon and color-coding for each status (green/red/gray/blue/orange) - Add dark mode support and accessibility attributes (ARIA labels) Enhance RevokeCertificate.tsx lookup flow - Import StatusBadge and utility functions - Add status validation in handleLookup() function - Check if certificate is NOT revocable before displaying preview - Show specific blocking message for non-active certificates - Replace hardcoded 'Active' badge with dynamic StatusBadge component Fixes mislabeling of expired/frozen certificates as 'Active' in revoke flow - Prevents users from attempting to revoke non-active certificates - Provides clear visual indication of certificate state - Improves user experience and reduces confusion Files: - NEW: frontend/src/components/StatusBadge.tsx (~90 lines) - MODIFIED: frontend/src/pages/RevokeCertificate.tsx (~3 lines) - NEW: REVOKE_CERTIFICATE_WALKTHROUGH.md (comprehensive documentation) --- REVOKE_CERTIFICATE_WALKTHROUGH.md | 346 +++++++++++++++++++++++ frontend/src/components/StatusBadge.tsx | 86 ++++++ frontend/src/pages/RevokeCertificate.tsx | 6 +- 3 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 REVOKE_CERTIFICATE_WALKTHROUGH.md create mode 100644 frontend/src/components/StatusBadge.tsx diff --git a/REVOKE_CERTIFICATE_WALKTHROUGH.md b/REVOKE_CERTIFICATE_WALKTHROUGH.md new file mode 100644 index 00000000..ead32fe8 --- /dev/null +++ b/REVOKE_CERTIFICATE_WALKTHROUGH.md @@ -0,0 +1,346 @@ +# RevokeCertificate Status Badge Fix - Implementation Walkthrough + +## Overview +This document describes the implementation of the fix for certificate status badge mislabeling and revocation flow blocking in the `RevokeCertificate.tsx` component. + +**Issue:** Expired and frozen certificates were displayed with "Active" badge and offered for revocation, misleading users about certificate state. + +--- + +## Problem Statement + +### Original Issues +1. **Lookup Logic Gap** (Lines 67-83) + - Only checked if `status === 'revoked'` + - All other statuses (expired, frozen, suspended) fell through to certificate preview + - No validation that certificate is in 'active' state + +2. **Hardcoded Badge** (Line 193) + - Badge always displayed "Active" regardless of actual status + - Visual mismatch with certificate state + - No distinction between active, expired, or frozen certificates + +3. **User Experience Impact** + - Expired certificates appeared revocable (but backend would reject) + - Frozen certificates appeared active + - Confusing and inconsistent interface + +--- + +## Solution Architecture + +### Component Design: StatusBadge.tsx + +**Purpose:** Centralized, reusable component for displaying certificate status + +**Supported Statuses:** +```typescript +type CertificateStatus = 'active' | 'revoked' | 'expired' | 'frozen' | 'suspended'; +``` + +**Design Pattern:** +- Configuration-driven rendering using `statusConfig` object +- Each status has: background, text color, icon, and label +- Exports utility functions for business logic +- Fully accessible with ARIA labels + +**Configuration Map:** +```typescript +const statusConfig = { + active: { bg: green, icon: CheckCircle, label: 'Active' }, + revoked: { bg: red, icon: XCircle, label: 'Revoked' }, + expired: { bg: gray, icon: Clock, label: 'Expired' }, + frozen: { bg: blue, icon: Lock, label: 'Frozen' }, + suspended: { bg: orange, icon: Pause, label: 'Suspended' } +} +``` + +--- + +## Implementation Details + +### 1. StatusBadge Component (`frontend/src/components/StatusBadge.tsx`) + +#### Component Function +```typescript +export const StatusBadge: React.FC = ({ status, className = '' }) => { + const config = statusConfig[status] || statusConfig.active; + + return ( + + {config.icon} + {config.label} + + ); +}; +``` + +#### Utility Functions + +**`isRevocableStatus(status: string): boolean`** +- Returns `true` only for `status === 'active'` +- Used in lookup logic to filter revocable certificates +- Centralizes business rule: only active certs can be revoked + +**`getRevocationBlockedMessage(status: string): string`** +- Maps status to user-friendly blocking message +- Examples: + - `'expired'` → "Cannot revoke an expired certificate." + - `'frozen'` → "Cannot revoke a frozen certificate. Please unfreeze it first." + - `'suspended'` → "Cannot revoke a suspended certificate. Please reinstate it first." +- Provides context to users on why action is blocked + +--- + +### 2. RevokeCertificate.tsx Updates + +#### Change 1: Import StatusBadge Utilities +```typescript +// Line 5 +import { StatusBadge, isRevocableStatus, getRevocationBlockedMessage } from '../components/StatusBadge'; +``` + +#### Change 2: Enhanced Lookup Logic +**Location:** Lines 69-72 in `handleLookup` function + +**Before:** +```typescript +if (result && result.status === 'revoked') { + setMessage({ type: 'warning', text: 'This certificate has already been revoked.' }); +} else if (result) { + // Accepts certificate with ANY status (problem!) + setCertificate({...}); +} +``` + +**After:** +```typescript +if (result && result.status === 'revoked') { + setMessage({ type: 'warning', text: 'This certificate has already been revoked.' }); +} else if (result && !isRevocableStatus(result.status)) { + // NEW: Check if certificate is in a non-active state + setMessage({ type: 'warning', text: getRevocationBlockedMessage(result.status) }); +} else if (result) { + // Now only proceeds if certificate is truly 'active' + setCertificate({...}); +} +``` + +**Flow:** +1. Check if already revoked → Show warning +2. Check if NOT active (expired/frozen/suspended) → Show specific blocking message +3. Only if active → Display certificate for revocation + +#### Change 3: Dynamic Status Badge +**Location:** Line 197 in Certificate Preview section + +**Before:** +```jsx + + Active + +``` + +**After:** +```jsx + +``` + +--- + +## Data Flow + +### Lookup and Filter Process +``` +User enters serial number and clicks "Search" + ↓ +handleLookup() called + ↓ +API: findCertBySerialNumber(serialNumber) + ↓ +Result received with { status, ...otherData } + ↓ + ┌─────────────────────────────────────┐ + │ Is status === 'revoked'? │ + └─────────────────────────────────────┘ + YES NO + ↓ ↓ + Show warning Is revocable (active)? + "Already revoked" YES NO + ↓ ↓ + Display Show blocked + for revoke message +``` + +### Status Badge Rendering +``` + + ↓ + statusConfig[status] + ↓ + Determine colors, icon, label + ↓ + Render with tailwind classes + ↓ + Display to user with context +``` + +--- + +## Testing Scenarios + +### Test Case 1: Active Certificate +- **Input:** Lookup active certificate +- **Expected:** Certificate shown in preview with "Active" badge (green) +- **Status:** ✅ Can proceed to revoke + +### Test Case 2: Already Revoked Certificate +- **Input:** Lookup revoked certificate +- **Expected:** Warning "This certificate has already been revoked" +- **Status:** ⚠️ Certificate not shown + +### Test Case 3: Expired Certificate +- **Input:** Lookup expired certificate +- **Expected:** Warning "Cannot revoke an expired certificate" +- **Status:** ⚠️ Certificate not shown (but could show preview if requested) + +### Test Case 4: Frozen Certificate +- **Input:** Lookup frozen certificate +- **Expected:** Warning "Cannot revoke a frozen certificate. Please unfreeze it first" +- **Status:** ⚠️ Certificate not shown + +### Test Case 5: Suspended Certificate +- **Input:** Lookup suspended certificate +- **Expected:** Warning "Cannot revoke a suspended certificate. Please reinstate it first" +- **Status:** ⚠️ Certificate not shown + +### Test Case 6: Certificate Not Found +- **Input:** Lookup non-existent serial number +- **Expected:** Error "Certificate not found" +- **Status:** ❌ No certificate shown + +--- + +## Code Quality & Best Practices + +### ✅ What Was Improved +- **Centralized Status Logic** - Single source of truth for status rules +- **Reusable Component** - StatusBadge can be used in certificate lists, verification pages +- **Type Safety** - TypeScript interfaces ensure correct status values +- **Accessibility** - ARIA labels and semantic HTML for screen readers +- **Dark Mode** - All badge styles include dark mode variants +- **Icon Support** - Visual indicators improve UX at a glance +- **Scalability** - Easy to add new statuses by extending `statusConfig` +- **User Communication** - Specific messages for each blocked scenario + +### 📋 Code Structure +``` +frontend/src/ +├── components/ +│ └── StatusBadge.tsx [NEW - Reusable status display] +│ ├── StatusBadgeProps interface +│ ├── statusConfig object +│ ├── StatusBadge component +│ ├── isRevocableStatus() utility +│ └── getRevocationBlockedMessage() utility +│ +└── pages/ + └── RevokeCertificate.tsx [MODIFIED - Enhanced lookup & badge] + ├── Import StatusBadge utilities + ├── Enhanced handleLookup() + └── Replace hardcoded badge +``` + +--- + +## Changes Summary + +### Files Modified: 1 +- `frontend/src/pages/RevokeCertificate.tsx` + - Lines 5: Added import + - Lines 69-72: Enhanced lookup logic (+3 lines) + - Line 197: Replace hardcoded badge (-1 line, +1 line) + +### Files Created: 1 +- `frontend/src/components/StatusBadge.tsx` (NEW) + - ~90 lines total + - Includes component, interfaces, configuration, utilities, documentation + +### Total Impact +``` +Insertions: ~92 lines +Deletions: ~1 line +Files Changed: 2 (1 new, 1 modified) +``` + +--- + +## Deployment Considerations + +### Frontend-Only Change +- No backend modifications needed +- No API changes required +- Backward compatible with existing certificate data + +### Browser Compatibility +- Uses modern React hooks and TypeScript +- Lucide React icons (existing dependency) +- Tailwind CSS utilities (existing dependency) +- No new external dependencies + +### Performance Impact +- StatusBadge is a pure component (no state) +- Minimal re-renders +- Icon imports are already cached by Lucide + +### Testing Requirements +- Unit tests for `isRevocableStatus()` function +- Unit tests for `getRevocationBlockedMessage()` function +- Integration tests for lookup flow with each status type +- Visual regression testing for badge rendering + +--- + +## Future Enhancements + +### Possible Extensions +1. **Extend StatusBadge usage** - Use in certificate lists, verification pages +2. **Add animations** - Subtle transitions when status changes +3. **Tooltip support** - Additional context on hover +4. **Status history** - Show timeline of status changes +5. **Localization** - i18n support for status messages +6. **Custom actions** - Different actions for frozen/suspended certs + +--- + +## Commit Message + +``` +fix(frontend): block non-active certificates from revocation and fix status badge + +- Create reusable StatusBadge component for certificate status display +- Add isRevocableStatus() utility to validate active certificates +- Add getRevocationBlockedMessage() for user-friendly blocking reasons +- Enhance RevokeCertificate.tsx lookup logic to check certificate status +- Block revocation of expired, frozen, and suspended certificates +- Replace hardcoded "Active" badge with dynamic StatusBadge component +- Support statuses: active, revoked, expired, frozen, suspended +- Include icon and color-coding for each status +- Add dark mode support and accessibility attributes + +This prevents users from attempting to revoke non-active certificates +and provides clear visual indication of certificate state. +``` + +--- + +## Related Issues +- Certificate mislabeling issue +- Revocation flow validation +- User experience consistency + +## Author +Implemented: August 30, 2026 + +## Status +✅ Implementation Complete - Ready for Testing & Review diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx new file mode 100644 index 00000000..9a355fc8 --- /dev/null +++ b/frontend/src/components/StatusBadge.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { CheckCircle, Clock, Lock, Pause, XCircle } from 'lucide-react'; + +interface StatusBadgeProps { + status: 'active' | 'revoked' | 'expired' | 'frozen' | 'suspended'; + className?: string; +} + +const statusConfig: Record = { + active: { + bg: 'bg-green-100 dark:bg-green-900/30', + text: 'text-green-700 dark:text-green-400', + icon: , + label: 'Active', + }, + revoked: { + bg: 'bg-red-100 dark:bg-red-900/30', + text: 'text-red-700 dark:text-red-400', + icon: , + label: 'Revoked', + }, + expired: { + bg: 'bg-gray-100 dark:bg-gray-900/30', + text: 'text-gray-700 dark:text-gray-400', + icon: , + label: 'Expired', + }, + frozen: { + bg: 'bg-blue-100 dark:bg-blue-900/30', + text: 'text-blue-700 dark:text-blue-400', + icon: , + label: 'Frozen', + }, + suspended: { + bg: 'bg-amber-100 dark:bg-amber-900/30', + text: 'text-amber-700 dark:text-amber-400', + icon: , + label: 'Suspended', + }, +}; + +/** + * Renders a status badge for a certificate + * Displays the certificate status with appropriate color, icon, and label + */ +export const StatusBadge: React.FC = ({ status, className = '' }) => { + const config = statusConfig[status] || statusConfig.active; + + return ( + + {config.icon} + {config.label} + + ); +}; + +/** + * Utility function to check if a certificate status is active (revocable) + */ +export const isRevocableStatus = (status: string): boolean => { + return status === 'active'; +}; + +/** + * Utility function to get a user-friendly message for why a certificate cannot be revoked + */ +export const getRevocationBlockedMessage = (status: string): string => { + switch (status) { + case 'revoked': + return 'This certificate has already been revoked.'; + case 'expired': + return 'Cannot revoke an expired certificate.'; + case 'frozen': + return 'Cannot revoke a frozen certificate. Please unfreeze it first.'; + case 'suspended': + return 'Cannot revoke a suspended certificate. Please reinstate it first.'; + default: + return 'Cannot revoke this certificate.'; + } +}; + +export default StatusBadge; diff --git a/frontend/src/pages/RevokeCertificate.tsx b/frontend/src/pages/RevokeCertificate.tsx index 7c831991..dbcce9ec 100644 --- a/frontend/src/pages/RevokeCertificate.tsx +++ b/frontend/src/pages/RevokeCertificate.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { findCertBySerialNumber, revokeCertificate } from '../api'; import { AlertTriangle, Search, ShieldAlert, CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import { StatusBadge, isRevocableStatus, getRevocationBlockedMessage } from '../components/StatusBadge'; import type { Certificate } from '../api'; interface Message { type: 'success' | 'error' | 'warning' | ''; @@ -66,6 +67,9 @@ const RevokeCertificate = () => { const result = await findCertBySerialNumber(serialNumber.trim()); if (result && result.status === 'revoked') { setMessage({ type: 'warning', text: 'This certificate has already been revoked.' }); + } else if (result && !isRevocableStatus(result.status)) { + // Block revocation for non-active statuses (expired, frozen, suspended) + setMessage({ type: 'warning', text: getRevocationBlockedMessage(result.status) }); } else if (result) { setCertificate({ id: result.id, @@ -190,7 +194,7 @@ const RevokeCertificate = () => { 2 Certificate Found - Active +
From 03347504fb4a8d8a02b72d4e9148f245c0d82a12 Mon Sep 17 00:00:00 2001 From: OmniZlatoon Date: Tue, 1 Sep 2026 06:03:30 +0100 Subject: [PATCH 3/4] chore: drop unrelated contract/doc changes --- stellar-contracts/src/lib.rs | 23 ++--------------------- stellar-contracts/src/test.rs | 15 ++------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/stellar-contracts/src/lib.rs b/stellar-contracts/src/lib.rs index 37fd739b..07c1e0d5 100644 --- a/stellar-contracts/src/lib.rs +++ b/stellar-contracts/src/lib.rs @@ -488,34 +488,15 @@ impl CertificateContract { // Store new certificate Self::set_persistent(&env, &DataKey::Certificate(new_id.clone()), &new_cert); - // Index the new certificate by issuer and owner - Self::append_cert_id(&env, DataKey::IssuerCertIds(issuer.clone()), new_id.clone()); - Self::append_cert_id(&env, DataKey::OwnerCertIds(new_cert.owner.clone()), new_id.clone()); - - // Mark original certificate as revoked (superseded) - let mut updated_original = original_cert; - updated_original.status = CertificateStatus::Revoked; - updated_original.revocation_reason = Some(String::from_str(&env, "Superseded")); - Self::set_persistent(&env, &DataKey::Certificate(old_id.clone()), &updated_original); - - // Emit issuance event for new certificate + // Emit issuance event env.events().publish( (symbol_short!("issued"), new_id.clone()), CertificateIssuedEvent { id: new_id, - issuer: issuer.clone(), + issuer, owner: new_cert.owner, }, ); - - // Emit revocation event for original certificate - env.events().publish( - (symbol_short!("revoked"), old_id.clone()), - CertificateRevokedEvent { - id: old_id, - reason: String::from_str(&env, "Superseded"), - }, - ); } // --- Certificate Transfer Functions --- diff --git a/stellar-contracts/src/test.rs b/stellar-contracts/src/test.rs index 1ec82562..af354bc3 100644 --- a/stellar-contracts/src/test.rs +++ b/stellar-contracts/src/test.rs @@ -150,21 +150,10 @@ fn test_reissue_certificate() { assert_eq!(new_cert.metadata_uri, new_metadata); assert_eq!(new_cert.parent_certificate_id, Some(old_id.clone())); - // Verify original certificate is now revoked (superseded) + // Verify original certificate still exists let original_cert = client.get_certificate(&old_id).expect("Certificate should exist"); assert_eq!(original_cert.id, old_id); - assert_eq!(original_cert.status, CertificateStatus::Revoked); - assert_eq!(original_cert.revocation_reason, Some(String::from_str(&env, "Superseded"))); - - // Verify new certificate is indexed by issuer - let issuer_certs = client.get_certificates_by_issuer(&issuer, &Pagination { page: 1, limit: 10 }); - let cert_ids: Vec = issuer_certs.data.iter().map(|c| c.id.clone()).collect(); - assert!(cert_ids.contains(&new_id), "New cert should be indexed by issuer"); - - // Verify new certificate is indexed by owner - let owner_certs = client.get_certificates_by_owner(&owner, &Pagination { page: 1, limit: 10 }); - let owner_cert_ids: Vec = owner_certs.data.iter().map(|c| c.id.clone()).collect(); - assert!(owner_cert_ids.contains(&new_id), "New cert should be indexed by owner"); + assert_eq!(original_cert.status, CertificateStatus::Active); // Original remains active } #[test] From 244cd1731af7219d290823565be7942503f5d965 Mon Sep 17 00:00:00 2001 From: OmniZlatoon Date: Tue, 1 Sep 2026 06:04:15 +0100 Subject: [PATCH 4/4] chore: remove stray doc artifacts --- REVOKE_CERTIFICATE_WALKTHROUGH.md | 346 ------------------------------ 1 file changed, 346 deletions(-) delete mode 100644 REVOKE_CERTIFICATE_WALKTHROUGH.md diff --git a/REVOKE_CERTIFICATE_WALKTHROUGH.md b/REVOKE_CERTIFICATE_WALKTHROUGH.md deleted file mode 100644 index ead32fe8..00000000 --- a/REVOKE_CERTIFICATE_WALKTHROUGH.md +++ /dev/null @@ -1,346 +0,0 @@ -# RevokeCertificate Status Badge Fix - Implementation Walkthrough - -## Overview -This document describes the implementation of the fix for certificate status badge mislabeling and revocation flow blocking in the `RevokeCertificate.tsx` component. - -**Issue:** Expired and frozen certificates were displayed with "Active" badge and offered for revocation, misleading users about certificate state. - ---- - -## Problem Statement - -### Original Issues -1. **Lookup Logic Gap** (Lines 67-83) - - Only checked if `status === 'revoked'` - - All other statuses (expired, frozen, suspended) fell through to certificate preview - - No validation that certificate is in 'active' state - -2. **Hardcoded Badge** (Line 193) - - Badge always displayed "Active" regardless of actual status - - Visual mismatch with certificate state - - No distinction between active, expired, or frozen certificates - -3. **User Experience Impact** - - Expired certificates appeared revocable (but backend would reject) - - Frozen certificates appeared active - - Confusing and inconsistent interface - ---- - -## Solution Architecture - -### Component Design: StatusBadge.tsx - -**Purpose:** Centralized, reusable component for displaying certificate status - -**Supported Statuses:** -```typescript -type CertificateStatus = 'active' | 'revoked' | 'expired' | 'frozen' | 'suspended'; -``` - -**Design Pattern:** -- Configuration-driven rendering using `statusConfig` object -- Each status has: background, text color, icon, and label -- Exports utility functions for business logic -- Fully accessible with ARIA labels - -**Configuration Map:** -```typescript -const statusConfig = { - active: { bg: green, icon: CheckCircle, label: 'Active' }, - revoked: { bg: red, icon: XCircle, label: 'Revoked' }, - expired: { bg: gray, icon: Clock, label: 'Expired' }, - frozen: { bg: blue, icon: Lock, label: 'Frozen' }, - suspended: { bg: orange, icon: Pause, label: 'Suspended' } -} -``` - ---- - -## Implementation Details - -### 1. StatusBadge Component (`frontend/src/components/StatusBadge.tsx`) - -#### Component Function -```typescript -export const StatusBadge: React.FC = ({ status, className = '' }) => { - const config = statusConfig[status] || statusConfig.active; - - return ( - - {config.icon} - {config.label} - - ); -}; -``` - -#### Utility Functions - -**`isRevocableStatus(status: string): boolean`** -- Returns `true` only for `status === 'active'` -- Used in lookup logic to filter revocable certificates -- Centralizes business rule: only active certs can be revoked - -**`getRevocationBlockedMessage(status: string): string`** -- Maps status to user-friendly blocking message -- Examples: - - `'expired'` → "Cannot revoke an expired certificate." - - `'frozen'` → "Cannot revoke a frozen certificate. Please unfreeze it first." - - `'suspended'` → "Cannot revoke a suspended certificate. Please reinstate it first." -- Provides context to users on why action is blocked - ---- - -### 2. RevokeCertificate.tsx Updates - -#### Change 1: Import StatusBadge Utilities -```typescript -// Line 5 -import { StatusBadge, isRevocableStatus, getRevocationBlockedMessage } from '../components/StatusBadge'; -``` - -#### Change 2: Enhanced Lookup Logic -**Location:** Lines 69-72 in `handleLookup` function - -**Before:** -```typescript -if (result && result.status === 'revoked') { - setMessage({ type: 'warning', text: 'This certificate has already been revoked.' }); -} else if (result) { - // Accepts certificate with ANY status (problem!) - setCertificate({...}); -} -``` - -**After:** -```typescript -if (result && result.status === 'revoked') { - setMessage({ type: 'warning', text: 'This certificate has already been revoked.' }); -} else if (result && !isRevocableStatus(result.status)) { - // NEW: Check if certificate is in a non-active state - setMessage({ type: 'warning', text: getRevocationBlockedMessage(result.status) }); -} else if (result) { - // Now only proceeds if certificate is truly 'active' - setCertificate({...}); -} -``` - -**Flow:** -1. Check if already revoked → Show warning -2. Check if NOT active (expired/frozen/suspended) → Show specific blocking message -3. Only if active → Display certificate for revocation - -#### Change 3: Dynamic Status Badge -**Location:** Line 197 in Certificate Preview section - -**Before:** -```jsx - - Active - -``` - -**After:** -```jsx - -``` - ---- - -## Data Flow - -### Lookup and Filter Process -``` -User enters serial number and clicks "Search" - ↓ -handleLookup() called - ↓ -API: findCertBySerialNumber(serialNumber) - ↓ -Result received with { status, ...otherData } - ↓ - ┌─────────────────────────────────────┐ - │ Is status === 'revoked'? │ - └─────────────────────────────────────┘ - YES NO - ↓ ↓ - Show warning Is revocable (active)? - "Already revoked" YES NO - ↓ ↓ - Display Show blocked - for revoke message -``` - -### Status Badge Rendering -``` - - ↓ - statusConfig[status] - ↓ - Determine colors, icon, label - ↓ - Render with tailwind classes - ↓ - Display to user with context -``` - ---- - -## Testing Scenarios - -### Test Case 1: Active Certificate -- **Input:** Lookup active certificate -- **Expected:** Certificate shown in preview with "Active" badge (green) -- **Status:** ✅ Can proceed to revoke - -### Test Case 2: Already Revoked Certificate -- **Input:** Lookup revoked certificate -- **Expected:** Warning "This certificate has already been revoked" -- **Status:** ⚠️ Certificate not shown - -### Test Case 3: Expired Certificate -- **Input:** Lookup expired certificate -- **Expected:** Warning "Cannot revoke an expired certificate" -- **Status:** ⚠️ Certificate not shown (but could show preview if requested) - -### Test Case 4: Frozen Certificate -- **Input:** Lookup frozen certificate -- **Expected:** Warning "Cannot revoke a frozen certificate. Please unfreeze it first" -- **Status:** ⚠️ Certificate not shown - -### Test Case 5: Suspended Certificate -- **Input:** Lookup suspended certificate -- **Expected:** Warning "Cannot revoke a suspended certificate. Please reinstate it first" -- **Status:** ⚠️ Certificate not shown - -### Test Case 6: Certificate Not Found -- **Input:** Lookup non-existent serial number -- **Expected:** Error "Certificate not found" -- **Status:** ❌ No certificate shown - ---- - -## Code Quality & Best Practices - -### ✅ What Was Improved -- **Centralized Status Logic** - Single source of truth for status rules -- **Reusable Component** - StatusBadge can be used in certificate lists, verification pages -- **Type Safety** - TypeScript interfaces ensure correct status values -- **Accessibility** - ARIA labels and semantic HTML for screen readers -- **Dark Mode** - All badge styles include dark mode variants -- **Icon Support** - Visual indicators improve UX at a glance -- **Scalability** - Easy to add new statuses by extending `statusConfig` -- **User Communication** - Specific messages for each blocked scenario - -### 📋 Code Structure -``` -frontend/src/ -├── components/ -│ └── StatusBadge.tsx [NEW - Reusable status display] -│ ├── StatusBadgeProps interface -│ ├── statusConfig object -│ ├── StatusBadge component -│ ├── isRevocableStatus() utility -│ └── getRevocationBlockedMessage() utility -│ -└── pages/ - └── RevokeCertificate.tsx [MODIFIED - Enhanced lookup & badge] - ├── Import StatusBadge utilities - ├── Enhanced handleLookup() - └── Replace hardcoded badge -``` - ---- - -## Changes Summary - -### Files Modified: 1 -- `frontend/src/pages/RevokeCertificate.tsx` - - Lines 5: Added import - - Lines 69-72: Enhanced lookup logic (+3 lines) - - Line 197: Replace hardcoded badge (-1 line, +1 line) - -### Files Created: 1 -- `frontend/src/components/StatusBadge.tsx` (NEW) - - ~90 lines total - - Includes component, interfaces, configuration, utilities, documentation - -### Total Impact -``` -Insertions: ~92 lines -Deletions: ~1 line -Files Changed: 2 (1 new, 1 modified) -``` - ---- - -## Deployment Considerations - -### Frontend-Only Change -- No backend modifications needed -- No API changes required -- Backward compatible with existing certificate data - -### Browser Compatibility -- Uses modern React hooks and TypeScript -- Lucide React icons (existing dependency) -- Tailwind CSS utilities (existing dependency) -- No new external dependencies - -### Performance Impact -- StatusBadge is a pure component (no state) -- Minimal re-renders -- Icon imports are already cached by Lucide - -### Testing Requirements -- Unit tests for `isRevocableStatus()` function -- Unit tests for `getRevocationBlockedMessage()` function -- Integration tests for lookup flow with each status type -- Visual regression testing for badge rendering - ---- - -## Future Enhancements - -### Possible Extensions -1. **Extend StatusBadge usage** - Use in certificate lists, verification pages -2. **Add animations** - Subtle transitions when status changes -3. **Tooltip support** - Additional context on hover -4. **Status history** - Show timeline of status changes -5. **Localization** - i18n support for status messages -6. **Custom actions** - Different actions for frozen/suspended certs - ---- - -## Commit Message - -``` -fix(frontend): block non-active certificates from revocation and fix status badge - -- Create reusable StatusBadge component for certificate status display -- Add isRevocableStatus() utility to validate active certificates -- Add getRevocationBlockedMessage() for user-friendly blocking reasons -- Enhance RevokeCertificate.tsx lookup logic to check certificate status -- Block revocation of expired, frozen, and suspended certificates -- Replace hardcoded "Active" badge with dynamic StatusBadge component -- Support statuses: active, revoked, expired, frozen, suspended -- Include icon and color-coding for each status -- Add dark mode support and accessibility attributes - -This prevents users from attempting to revoke non-active certificates -and provides clear visual indication of certificate state. -``` - ---- - -## Related Issues -- Certificate mislabeling issue -- Revocation flow validation -- User experience consistency - -## Author -Implemented: August 30, 2026 - -## Status -✅ Implementation Complete - Ready for Testing & Review