From 28a13d619c10cf28f06b770972f6a5151be51d4a Mon Sep 17 00:00:00 2001 From: Sidharth Prabhu Date: Wed, 29 Jul 2026 11:52:09 +0530 Subject: [PATCH] Updated API Dashboard --- api-dashboard/src/App.css | 145 ++++ api-dashboard/src/App.tsx | 799 +++++++++++++++--- .../config/DeveloperApiLogInterceptor.java | 61 ++ .../config/DeveloperApiResponseAdvice.java | 43 + .../canteen/sales/config/SecurityConfig.java | 1 + .../rit/canteen/sales/config/WebConfig.java | 19 +- .../controller/DeveloperApiKeyController.java | 38 +- .../controller/DeveloperAppController.java | 129 +++ .../canteen/sales/model/DeveloperApiKey.java | 11 + .../canteen/sales/model/DeveloperApiLog.java | 138 +++ .../rit/canteen/sales/model/DeveloperApp.java | 94 +++ .../repository/DeveloperApiKeyRepository.java | 2 + .../repository/DeveloperApiLogRepository.java | 10 + .../repository/DeveloperAppRepository.java | 9 + .../sales/service/DeveloperApiKeyService.java | 9 + frontend/README.md | 4 +- generate_report.py | 4 +- package-lock.json | 2 +- test_qr_print.html | 4 +- 19 files changed, 1386 insertions(+), 136 deletions(-) create mode 100644 backend/src/main/java/com/rit/canteen/sales/config/DeveloperApiLogInterceptor.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/config/DeveloperApiResponseAdvice.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/controller/DeveloperAppController.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/model/DeveloperApiLog.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/model/DeveloperApp.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/repository/DeveloperApiLogRepository.java create mode 100644 backend/src/main/java/com/rit/canteen/sales/repository/DeveloperAppRepository.java diff --git a/api-dashboard/src/App.css b/api-dashboard/src/App.css index 420f30e4..17ab110a 100644 --- a/api-dashboard/src/App.css +++ b/api-dashboard/src/App.css @@ -1238,3 +1238,148 @@ input:checked + .slider:before { border-radius: 0.25rem; border: 1px solid rgba(0, 0, 0, 0.05); } + +.log-row-hover:hover { + background-color: rgba(0, 24, 40, 0.02); +} + +/* Mobile responsive navigation & header */ +.mobile-header { + display: none; + align-items: center; + justify-content: space-between; + padding: 0.85rem 1.25rem; + background: rgba(255, 255, 255, 0.85); + border-bottom: 1px solid var(--color-border); + position: sticky; + top: 0; + z-index: 150; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.mobile-menu-toggle { + background: transparent; + border: none; + color: var(--color-text-dark); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + border-radius: 0.5rem; + transition: background 0.2s; +} + +.mobile-menu-toggle:hover { + background: rgba(0, 0, 0, 0.05); +} + +.mobile-brand-title { + font-size: 1rem; + font-weight: 800; + color: var(--color-text-dark); + letter-spacing: -0.01em; +} + +.sidebar-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 190; + animation: fadeIn 0.2s ease-out forwards; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@media (max-width: 768px) { + .mobile-header { + display: flex; + } + + .dashboard-sidebar { + transform: translateX(-100%); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 10px 0 30px rgba(0,0,0,0.15); + z-index: 200; + } + + .dashboard-sidebar.open { + transform: translateX(0); + } + + .dashboard-main-content { + margin-left: 0; + padding: 1.25rem 1rem; + min-height: calc(100vh - 55px); + } + + .panel-header h2 { + font-size: 1.25rem; + } + + .panel-header p { + font-size: 0.85rem; + } + + .premium-card { + padding: 1.25rem; + border-radius: 1rem; + } + + /* Form & Key display adjustments */ + .key-display-box { + flex-wrap: wrap !important; + gap: 0.5rem !important; + padding: 0.75rem !important; + } + + .key-code { + font-size: 0.75rem !important; + word-break: break-all !important; + white-space: normal !important; + line-height: 1.4; + } + + .limit-indicator { + align-self: stretch; + justify-content: center; + } + + /* Stack columns vertically on small screens */ + .keys-grid, + .sandbox-workspace { + grid-template-columns: 1fr !important; + gap: 1.25rem; + } + + /* Stack logs split pane details below table on small screens */ + .keys-grid + div, + div[style*="display: flex; gap: 1.5rem;"] { + flex-direction: column !important; + gap: 1rem !important; + } + + /* Make sure split pane has full width on mobile */ + div[style*="width: 400px;"] { + width: 100% !important; + } + + /* Login optimizations */ + .login-screen-wrapper { + padding: 1rem !important; + } + + .login-card { + padding: 1.5rem 1.25rem !important; + gap: 1.25rem !important; + } +} diff --git a/api-dashboard/src/App.tsx b/api-dashboard/src/App.tsx index ecf9d7eb..f478cba3 100644 --- a/api-dashboard/src/App.tsx +++ b/api-dashboard/src/App.tsx @@ -16,6 +16,7 @@ interface DeveloperApiKey { lastUsedAt?: string; writeAccess?: boolean; permissions?: string; + appId?: number; } interface UserSession { @@ -52,8 +53,19 @@ function App() { 'READ_PRODUCTS', 'READ_STALLS', 'READ_ORDERS', 'READ_WALLETS' ]); + // Apps Management State + const [apps, setApps] = useState([]); + const [newAppName, setNewAppName] = useState(''); + const [newAppDesc, setNewAppDesc] = useState(''); + // UI Tabs & Feedback - const [activeTab, setActiveTab] = useState<'keys' | 'playground' | 'docs'>('keys'); + const [activeTab, setActiveTab] = useState<'keys' | 'playground' | 'docs' | 'logs'>('keys'); + const [logs, setLogs] = useState([]); + const [logPage, setLogPage] = useState(0); + const [logTotalPages, setLogTotalPages] = useState(0); + const [logTotalItems, setLogTotalItems] = useState(0); + const [selectedLog, setSelectedLog] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); @@ -84,12 +96,16 @@ function App() { } }, []); - // Fetch keys when session changes + // Fetch keys and apps when session changes useEffect(() => { if (session) { fetchKeys(); + if (session.roleOrType === 'CUSTOMER') { + fetchApps(); + } } else { setKeys([]); + setApps([]); } }, [session]); @@ -240,6 +256,142 @@ function App() { } }; + const fetchApps = async () => { + if (!session) return; + try { + const res = await fetch(`${BACKEND_URL}/api/developer-apps`, { + headers: { + 'Authorization': `Bearer ${session.token}`, + }, + }); + if (!res.ok) throw new Error('Failed to fetch apps'); + const data = await res.json(); + setApps(data); + } catch (err: any) { + showNotification('error', err.message); + } + }; + + const handleCreateApp = async (e: React.FormEvent) => { + e.preventDefault(); + if (!session) return; + if (!newAppName.trim()) { + showNotification('error', 'Please enter a name for the app'); + return; + } + setLoading(true); + clearMessages(); + try { + const res = await fetch(`${BACKEND_URL}/api/developer-apps`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.token}`, + }, + body: JSON.stringify({ + name: newAppName, + description: newAppDesc + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create app'); + showNotification('success', 'App registered successfully'); + setNewAppName(''); + setNewAppDesc(''); + fetchApps(); + } catch (err: any) { + showNotification('error', err.message); + } finally { + setLoading(false); + } + }; + + const handleDeleteApp = async (appId: number) => { + if (!session) return; + if (!window.confirm('Are you sure you want to delete this App? This will also revoke its associated API key.')) return; + setLoading(true); + clearMessages(); + try { + const res = await fetch(`${BACKEND_URL}/api/developer-apps/${appId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${session.token}`, + }, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to delete app'); + showNotification('success', 'App deleted successfully'); + fetchApps(); + fetchKeys(); + } catch (err: any) { + showNotification('error', err.message); + } finally { + setLoading(false); + } + }; + + const handleCreateKeyForApp = async (appId: number, name: string) => { + if (!session) return; + setLoading(true); + clearMessages(); + try { + const permissionsPayload = 'READ_PRODUCTS,READ_STALLS,READ_ORDERS,READ_WALLETS'; + const writeAccessPayload = false; + + const res = await fetch(`${BACKEND_URL}/api/developer-keys`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.token}`, + }, + body: JSON.stringify({ + name: name, + writeAccess: writeAccessPayload, + permissions: permissionsPayload, + appId: appId + }), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to generate key'); + + showNotification('success', 'API Key generated successfully'); + fetchKeys(); + } catch (err: any) { + showNotification('error', err.message); + } finally { + setLoading(false); + } + }; + + const fetchLogs = async (page = 0) => { + if (!session) return; + setLoading(true); + try { + const res = await fetch(`${BACKEND_URL}/api/developer-keys/logs?page=${page}&size=10`, { + headers: { + 'Authorization': `Bearer ${session.token}`, + }, + }); + if (!res.ok) throw new Error('Failed to fetch API logs'); + const data = await res.json(); + setLogs(data.content); + setLogPage(data.currentPage); + setLogTotalPages(data.totalPages); + setLogTotalItems(data.totalItems); + } catch (err: any) { + showNotification('error', err.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (activeTab === 'logs' && session) { + fetchLogs(0); + } + }, [activeTab, session]); + const handleCreateKey = async (e: React.FormEvent) => { e.preventDefault(); if (!session) return; @@ -700,8 +852,25 @@ function App() { ) : ( <> + {/* Mobile Top Navigation Header */} +
+ +
Tillo Dev Portal
+
+ + {/* Sidebar Backdrop Overlay on Mobile */} + {sidebarOpen && ( +
setSidebarOpen(false)} /> + )} + {/* Full Height left sidebar */} -
{/* API Key list */} -
-
-

Generate New API Key

-

Assign a friendly name to describe your application module usage.

- -
-
- - setNewKeyName(e.target.value)} - /> -
+ {isSystemUser ? ( +
+
+

Generate New API Key

+

Assign a friendly name to describe your application module usage.

+ + +
+ + setNewKeyName(e.target.value)} + /> +
- {isSystemUser && (
@@ -906,115 +1089,489 @@ function App() { })}
- )} - - - + + + - {isSystemUser ? (
Unlimited Staff API Keys Allowed
- ) : ( +
+ +
+ {keys.length === 0 ? ( +
+
🔐
+

No Active API Keys

+

You haven't generated any API keys yet. Create one on the left to start accessing endpoints.

+
+ ) : ( + keys.map((key) => ( +
+
+
+

{key.name}

+
+ {key.userType} Key + + {key.writeAccess ? 'Elevated (Write)' : 'Read-Only'} + +
+
+ +
+ +
+ + {showSecrets[key.id] ? key.apiKey : `${key.apiKey.substring(0, 8)}••••••••••••••••••••••••••••`} + +
+ + + +
+
+ + {key.permissions && ( +
+ Scopes: +
+ {key.permissions.split(',').map(scope => ( + {scope} + ))} +
+
+ )} + +
+ Created: {new Date(key.createdAt).toLocaleDateString()} + Last Used: {key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleTimeString() : 'Never'} +
+
+ )) + )} +
+
+ ) : ( +
+ {/* CUSTOMER Mode: Register App and show App based Keys */} +
+

Register New App

+

First create an app to configure features and obtain its API credentials.

+ +
+
+ + setNewAppName(e.target.value)} + required + /> +
+ +
+ +