Files
explorer-monorepo/frontend/src/components/access/AccessManagementPage.tsx
defiQUG 0cb31cfa9d
Some checks failed
Validate Explorer / frontend (push) Failing after 14m45s
Deploy Explorer Live / deploy (push) Failing after 14m52s
Validate Explorer / smoke-e2e (push) Has been cancelled
Improve explorer SSR, hydration, compaction, and smoke coverage.
Defer heavy getServerSideProps on home, operator, addresses, and wallet to cut TTFB; centralize locale-safe formatters with client-only relative times; add compact ops UX, bridge/operator relay pagination, and Playwright route/scroll smoke in deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-22 15:52:47 -07:00

881 lines
42 KiB
TypeScript

import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
import SectionTabs, { TabPanel, type SectionTab } from '@/components/common/SectionTabs'
import DisclosureSection from '@/components/common/DisclosureSection'
import EntityBadge from '@/components/common/EntityBadge'
import {
accessApi,
type AccessAPIKeyRecord,
type AccessAuditEntry,
type AccessProduct,
type AccessSubscription,
type AccessUsageSummary,
type AccessUser,
type WalletAccessSession,
} from '@/services/api/access'
import { formatDate, formatInteger, formatTimestamp } from '@/utils/format'
const ACCESS_SCOPE_OPTIONS = ['rpc:read', 'rpc:write', 'rpc:admin'] as const
const OPERATOR_IDENTITIES = [
{
slug: 'thirdweb-rpc',
label: 'ThirdWeb',
vmid: 2103,
address: '0xB2dEA0e264ddfFf91057A3415112e57A1a5Eac14',
},
{
slug: 'alltra-rpc',
label: 'Alltra/HYBX',
vmid: 2102,
address: '0xaf6e3444AEaf7855cf41b557C94A96dc7fcF49C1',
},
{
slug: 'core-rpc',
label: 'DBIS',
vmid: 2101,
address: '0x4A666F96fC8764181194447A7dFdb7d471b301C8',
},
] as const
function Field({
label,
value,
onChange,
type = 'text',
}: {
label: string
value: string
onChange: (value: string) => void
type?: string
}) {
return (
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{label}</span>
<input
type={type}
value={value}
onChange={(event) => onChange(event.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
/>
</label>
)
}
export default function AccessManagementPage() {
const [products, setProducts] = useState<AccessProduct[]>([])
const [user, setUser] = useState<AccessUser | null>(null)
const [walletSession, setWalletSession] = useState<WalletAccessSession | null>(null)
const [connectingWallet, setConnectingWallet] = useState(false)
const [apiKeys, setApiKeys] = useState<AccessAPIKeyRecord[]>([])
const [subscriptions, setSubscriptions] = useState<AccessSubscription[]>([])
const [usage, setUsage] = useState<AccessUsageSummary[]>([])
const [auditEntries, setAuditEntries] = useState<AccessAuditEntry[]>([])
const [adminSubscriptions, setAdminSubscriptions] = useState<AccessSubscription[]>([])
const [adminAuditEntries, setAdminAuditEntries] = useState<AccessAuditEntry[]>([])
const [auditLimit, setAuditLimit] = useState('20')
const [adminAuditLimit, setAdminAuditLimit] = useState('50')
const [adminSubscriptionStatus, setAdminSubscriptionStatus] = useState('pending')
const [adminAuditProduct, setAdminAuditProduct] = useState('')
const [adminActionNotes, setAdminActionNotes] = useState<Record<string, string>>({})
const [email, setEmail] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [apiKeyName, setAPIKeyName] = useState('Core RPC key')
const [apiKeyTier, setAPIKeyTier] = useState('pro')
const [apiKeyProduct, setAPIKeyProduct] = useState('thirdweb-rpc')
const [apiKeyExpiresDays, setAPIKeyExpiresDays] = useState('30')
const [apiKeyMonthlyQuota, setAPIKeyMonthlyQuota] = useState('')
const [apiKeyScopes, setAPIKeyScopes] = useState<string[]>(['rpc:read', 'rpc:write'])
const [createdKey, setCreatedKey] = useState('')
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const [accessTab, setAccessTab] = useState<'products' | 'account' | 'admin'>('products')
const accessTabs = useMemo((): SectionTab<'products' | 'account' | 'admin'>[] => {
const tabs: SectionTab<'products' | 'account' | 'admin'>[] = [
{ id: 'products', label: 'Products', count: products.length },
{ id: 'account', label: 'Account & keys' },
]
if (user?.is_admin) {
tabs.push({ id: 'admin', label: 'Admin' })
}
return tabs
}, [products.length, user?.is_admin])
const clearSessionState = useCallback(() => {
setUser(null)
setApiKeys([])
setSubscriptions([])
setUsage([])
setAuditEntries([])
setAdminSubscriptions([])
setAdminAuditEntries([])
}, [])
const syncWalletSession = useCallback(() => {
setWalletSession(accessApi.getStoredWalletSession())
}, [])
const loadAdminData = useCallback(async (
isAdmin: boolean,
nextSubscriptionStatus = adminSubscriptionStatus,
nextAuditProduct = adminAuditProduct,
nextAuditLimit = Number(adminAuditLimit),
) => {
if (!isAdmin) {
setAdminSubscriptions([])
setAdminAuditEntries([])
return
}
const [adminResponse, adminAuditResponse] = await Promise.all([
accessApi.listAdminSubscriptions(nextSubscriptionStatus).catch(() => ({ subscriptions: [] })),
accessApi.listAdminAudit(nextAuditLimit, nextAuditProduct).catch(() => ({ entries: [] })),
])
setAdminSubscriptions(adminResponse.subscriptions || [])
setAdminAuditEntries(adminAuditResponse.entries || [])
}, [adminAuditLimit, adminAuditProduct, adminSubscriptionStatus])
const loadSignedInData = useCallback(async () => {
const [me, keys, usageResponse, auditResponse] = await Promise.all([
accessApi.getMe(),
accessApi.listAPIKeys(),
accessApi.getUsage().catch(() => ({ usage: [] })),
accessApi.listAudit(Number(auditLimit)).catch(() => ({ entries: [] })),
])
setUser(me.user)
setSubscriptions(me.subscriptions || [])
setApiKeys(keys.api_keys || [])
setUsage(usageResponse.usage || [])
setAuditEntries(auditResponse.entries || [])
await loadAdminData(Boolean(me.user?.is_admin))
}, [auditLimit, loadAdminData])
const loadAccessData = useCallback(async () => {
const productResponse = await accessApi.listProducts()
setProducts(productResponse.products || [])
syncWalletSession()
const token = accessApi.getStoredAccessToken()
if (!token) {
clearSessionState()
return
}
try {
await loadSignedInData()
} catch {
accessApi.clearSession()
clearSessionState()
}
}, [clearSessionState, loadSignedInData, syncWalletSession])
useEffect(() => {
void loadAccessData()
}, [loadAccessData])
useEffect(() => {
syncWalletSession()
window.addEventListener('storage', syncWalletSession)
window.addEventListener('explorer-access-session-changed', syncWalletSession)
return () => {
window.removeEventListener('storage', syncWalletSession)
window.removeEventListener('explorer-access-session-changed', syncWalletSession)
}
}, [syncWalletSession])
useEffect(() => {
if (!user) return
void accessApi
.listAudit(Number(auditLimit))
.then((response) => setAuditEntries(response.entries || []))
.catch(() => {})
}, [auditLimit, user])
useEffect(() => {
if (!user?.is_admin) return
void loadAdminData(true)
}, [adminSubscriptionStatus, adminAuditLimit, adminAuditProduct, loadAdminData, user?.is_admin])
useEffect(() => {
if (apiKeyProduct === 'core-rpc') {
setAPIKeyScopes((current) =>
current.includes('rpc:admin') ? current : [...current, 'rpc:admin'],
)
} else {
setAPIKeyScopes((current) => current.filter((scope) => scope !== 'rpc:admin'))
}
}, [apiKeyProduct])
const handleRegister = async (event: FormEvent) => {
event.preventDefault()
setError('')
setMessage('')
try {
const response = await accessApi.register(email, username, password)
setUser(response.user)
setMessage('Account created. You can now issue API keys for managed RPC access.')
await loadSignedInData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed')
}
}
const handleLogin = async (event: FormEvent) => {
event.preventDefault()
setError('')
setMessage('')
try {
const response = await accessApi.login(email, password)
setUser(response.user)
await loadSignedInData()
setMessage('Signed in successfully.')
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed')
}
}
const handleCreateAPIKey = async (event: FormEvent) => {
event.preventDefault()
setError('')
setMessage('')
setCreatedKey('')
try {
const response = await accessApi.createAPIKey({
name: apiKeyName,
tier: apiKeyTier,
productSlug: apiKeyProduct,
expiresDays: apiKeyExpiresDays === 'never' ? 0 : Number(apiKeyExpiresDays || 0),
monthlyQuota: apiKeyMonthlyQuota.trim() ? Number(apiKeyMonthlyQuota) : undefined,
scopes: apiKeyScopes,
})
setCreatedKey(response.api_key)
setMessage('API key created. This is the only time the plaintext key will be shown.')
await loadSignedInData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create API key')
}
}
const handleRotate = async (key: AccessAPIKeyRecord) => {
setError('')
setMessage('')
setCreatedKey('')
try {
const response = await accessApi.createAPIKey({
name: key.name.replace(/\s+\[[^\]]+\]$/, ''),
tier: key.tier,
productSlug: key.productSlug,
monthlyQuota: key.monthlyQuota,
scopes: key.scopes,
})
await accessApi.revokeAPIKey(key.id)
setCreatedKey(response.api_key)
setMessage('API key rotated. The old key has been revoked and the new plaintext key is shown below once.')
await loadSignedInData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to rotate API key')
}
}
const handleRevoke = async (id: string) => {
setError('')
setMessage('')
try {
await accessApi.revokeAPIKey(id)
setMessage('API key revoked.')
await loadSignedInData()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to revoke API key')
}
}
const handleSignOut = () => {
accessApi.clearSession()
clearSessionState()
setCreatedKey('')
setMessage('Signed out.')
}
const handleWalletConnect = async () => {
setError('')
setMessage('')
try {
setConnectingWallet(true)
const session = await accessApi.connectWalletSession()
setWalletSession(session)
await loadSignedInData()
setMessage('Wallet connected. Account sign-in is active and authenticated explorer access is now available.')
} catch (err) {
setError(err instanceof Error ? err.message : 'Wallet connection failed')
} finally {
setConnectingWallet(false)
}
}
const handleWalletDisconnect = () => {
accessApi.clearWalletSession()
syncWalletSession()
clearSessionState()
setCreatedKey('')
setMessage('Wallet session disconnected.')
}
const handleRequestSubscription = async (productSlug: string, tier: string) => {
setError('')
setMessage('')
try {
await accessApi.requestSubscription(productSlug, tier)
await loadSignedInData()
setMessage('Access request saved.')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to request access')
}
}
const handleAdminSubscriptionAction = async (subscriptionId: string, status: string) => {
setError('')
setMessage('')
try {
await accessApi.updateAdminSubscription(subscriptionId, status, adminActionNotes[subscriptionId] || '')
await loadSignedInData()
setAdminActionNotes((current) => ({ ...current, [subscriptionId]: '' }))
setMessage(`Subscription ${status === 'active' ? 'approved' : status}.`)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update subscription')
}
}
const getSubscriptionForProduct = (productSlug: string) =>
subscriptions.find((subscription) => subscription.productSlug === productSlug)
const handleScopeToggle = (scope: string) => {
setAPIKeyScopes((current) =>
current.includes(scope) ? current.filter((entry) => entry !== scope) : [...current, scope],
)
}
const handleAdminAuditProductChange = async (value: string) => {
setAdminAuditProduct(value)
}
const getOperatorIdentity = (productSlug: string) =>
OPERATOR_IDENTITIES.find((entry) => entry.slug === productSlug)
return (
<main className="container mx-auto px-4 py-4 sm:py-6">
<PageIntro
compact
eyebrow="Access Control"
title="Wallet Login, RPC Access & API Tokens"
description="Wallet sign-in, API keys, and subscription-gated RPC products."
actions={[
{ href: '/wallet', label: 'Wallet' },
{ href: '/system', label: 'System' },
]}
/>
{message ? (
<Card className="mb-6 border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20">
<p className="text-sm text-emerald-900 dark:text-emerald-100">{message}</p>
</Card>
) : null}
{error ? (
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
<p className="text-sm text-red-900 dark:text-red-100">{error}</p>
</Card>
) : null}
<SectionTabs
idPrefix="access"
ariaLabel="Access console sections"
tabs={accessTabs}
activeTab={accessTab}
onChange={setAccessTab}
className="mb-4"
/>
<TabPanel idPrefix="access" tabId="products" activeTab={accessTab}>
<div className="mb-4 grid gap-4 lg:grid-cols-3">
{products.map((product) => (
<Card key={product.slug} title={product.name}>
<div className="space-y-3 text-sm text-gray-700 dark:text-gray-300">
<div className="flex flex-wrap gap-2">
<EntityBadge label={product.provider} tone="info" />
<EntityBadge label={`vmid ${product.vmid}`} />
<EntityBadge label={product.default_tier} tone="success" />
<EntityBadge label={product.billing_model} tone="warning" />
{product.requires_approval ? <EntityBadge label="approval required" tone="warning" /> : <EntityBadge label="self-service" tone="success" />}
</div>
<p>{product.description}</p>
<div>
<div className="font-semibold text-gray-900 dark:text-white">HTTP</div>
<code className="break-all text-xs">{product.http_url}</code>
</div>
{product.ws_url ? (
<div>
<div className="font-semibold text-gray-900 dark:text-white">WS</div>
<code className="break-all text-xs">{product.ws_url}</code>
</div>
) : null}
<div>
<div className="font-semibold text-gray-900 dark:text-white">Use cases</div>
<div className="mt-2 flex flex-wrap gap-2">
{product.use_cases.map((item) => (
<EntityBadge key={item} label={item} className="normal-case tracking-normal" />
))}
</div>
</div>
{getOperatorIdentity(product.slug) ? (
<div>
<div className="font-semibold text-gray-900 dark:text-white">Primary operator / deployer</div>
<div className="mt-2 flex flex-wrap gap-2">
<EntityBadge label={getOperatorIdentity(product.slug)?.label || product.provider} tone="info" />
<EntityBadge label={`vmid ${getOperatorIdentity(product.slug)?.vmid || product.vmid}`} />
</div>
<code className="mt-2 block break-all text-xs">{getOperatorIdentity(product.slug)?.address}</code>
</div>
) : null}
{user ? (
<div className="border-t border-gray-200 pt-3 dark:border-gray-700">
{(() => {
const subscription = getSubscriptionForProduct(product.slug)
if (subscription) {
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
<EntityBadge label={subscription.status} tone={subscription.status === 'active' ? 'success' : 'warning'} />
<EntityBadge label={subscription.tier} />
<EntityBadge label={`${subscription.requestsUsed}/${subscription.monthlyQuota || 0}`} tone="info" />
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{subscription.notes || 'Subscription record present.'}
</div>
</div>
)
}
return (
<button
type="button"
onClick={() => void handleRequestSubscription(product.slug, product.default_tier)}
className="rounded-lg bg-primary-600 px-3 py-2 text-sm font-medium text-white hover:bg-primary-700"
>
{product.requires_approval ? 'Request access' : 'Activate access'}
</button>
)
})()}
</div>
) : null}
</div>
</Card>
))}
</div>
</TabPanel>
<TabPanel idPrefix="access" tabId="account" activeTab={accessTab}>
<div className="grid gap-4 lg:grid-cols-[1fr_1.2fr]">
<div className="space-y-4">
<DisclosureSection title="Wallet authentication" defaultOpen={Boolean(walletSession)} alwaysCollapsible>
<Card>
<div className="space-y-4 text-sm text-gray-700 dark:text-gray-300">
<p>
Use a connected wallet for standard account sign-in, then access subscriptions, API keys, and managed RPC controls with the same authenticated session.
</p>
{walletSession ? (
<div className="space-y-3 rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-wrap gap-2">
<EntityBadge label="wallet sign-in active" tone="success" />
<EntityBadge label={walletSession.track} tone="info" />
{walletSession.permissions.map((permission) => (
<EntityBadge key={permission} label={permission} className="normal-case tracking-normal" />
))}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Your wallet address remains private within the access console. This session is treated as account sign-in, not a public identifier.
</p>
<div className="text-xs text-gray-500 dark:text-gray-400">
Session expires {formatTimestamp(walletSession.expiresAt)}
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={handleWalletDisconnect}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-800 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
>
Disconnect wallet
</button>
<Link href="/wallet" className="rounded-lg border border-primary-300 px-4 py-2 text-sm font-medium text-primary-700 hover:bg-primary-50 dark:border-primary-800 dark:text-primary-300 dark:hover:bg-primary-950/20">
Open wallet tools
</Link>
</div>
</div>
) : (
<div className="rounded-2xl border border-dashed border-primary-300 bg-primary-50/60 p-4 dark:border-primary-700/50 dark:bg-primary-950/20">
<div className="mb-3 text-sm text-primary-900 dark:text-primary-100">
No wallet session is active. Connect a browser wallet to sign in to your account and unlock the access-management plane.
</div>
<button
type="button"
onClick={() => void handleWalletConnect()}
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
>
{connectingWallet ? 'Connecting wallet…' : 'Connect wallet'}
</button>
</div>
)}
</div>
</Card>
</DisclosureSection>
<DisclosureSection title="Operator identities" defaultOpen={false} alwaysCollapsible>
<Card>
<div className="space-y-3">
{OPERATOR_IDENTITIES.map((identity) => (
<div key={identity.slug} className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-wrap gap-2">
<EntityBadge label={identity.label} tone="info" />
<EntityBadge label={identity.slug} />
<EntityBadge label={`vmid ${identity.vmid}`} tone="warning" />
</div>
<code className="mt-2 block break-all text-xs text-gray-700 dark:text-gray-300">{identity.address}</code>
</div>
))}
</div>
</Card>
</DisclosureSection>
<Card title={user ? `Signed in as ${user.username}` : 'Create or Access Account'}>
{user ? (
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
{user.email}
</p>
<button
type="button"
onClick={handleSignOut}
className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white"
>
Sign out
</button>
</div>
) : (
<div className="space-y-6">
<form onSubmit={handleRegister} className="space-y-3">
<Field label="Email" value={email} onChange={setEmail} type="email" />
<Field label="Username" value={username} onChange={setUsername} />
<Field label="Password" value={password} onChange={setPassword} type="password" />
<button type="submit" className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700">
Register
</button>
</form>
<form onSubmit={handleLogin} className="space-y-3 border-t border-gray-200 pt-4 dark:border-gray-700">
<Field label="Email" value={email} onChange={setEmail} type="email" />
<Field label="Password" value={password} onChange={setPassword} type="password" />
<button type="submit" className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white">
Sign in
</button>
</form>
</div>
)}
</Card>
{user ? (
<Card title="Create API Key">
<form onSubmit={handleCreateAPIKey} className="space-y-3">
<Field label="Key name" value={apiKeyName} onChange={setAPIKeyName} />
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Tier</span>
<select value={apiKeyTier} onChange={(event) => setAPIKeyTier(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
<option value="free">free</option>
<option value="pro">pro</option>
<option value="enterprise">enterprise</option>
</select>
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Product</span>
<select value={apiKeyProduct} onChange={(event) => setAPIKeyProduct(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
{products.map((product) => (
<option key={product.slug} value={product.slug}>{product.name}</option>
))}
</select>
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Expiry</span>
<select value={apiKeyExpiresDays} onChange={(event) => setAPIKeyExpiresDays(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
<option value="7">7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="365">365 days</option>
<option value="never">No expiry</option>
</select>
</label>
<Field label="Monthly quota override (optional)" value={apiKeyMonthlyQuota} onChange={setAPIKeyMonthlyQuota} />
<div>
<span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">Scopes</span>
<div className="flex flex-wrap gap-2">
{ACCESS_SCOPE_OPTIONS.map((scope) => (
<label key={scope} className="inline-flex items-center gap-2 rounded-full border border-gray-300 px-3 py-2 text-sm dark:border-gray-700">
<input
type="checkbox"
checked={apiKeyScopes.includes(scope)}
onChange={() => handleScopeToggle(scope)}
/>
<span>{scope}</span>
</label>
))}
</div>
</div>
<button type="submit" className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700">
Issue key
</button>
</form>
{createdKey ? (
<div className="mt-4 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900/50 dark:bg-amber-950/20">
<div className="mb-2 text-sm font-semibold text-amber-900 dark:text-amber-100">Plaintext API key</div>
<code className="block break-all text-xs text-amber-900 dark:text-amber-100">{createdKey}</code>
</div>
) : null}
</Card>
) : null}
</div>
<Card title="Issued API Keys">
{user ? (
apiKeys.length > 0 ? (
<div className="space-y-4">
{apiKeys.map((key) => (
<div key={key.id} className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="font-semibold text-gray-900 dark:text-white">{key.name}</div>
<div className="mt-2 flex flex-wrap gap-2">
<EntityBadge label={key.tier} tone="success" />
{key.productSlug ? <EntityBadge label={key.productSlug} tone="info" /> : null}
<EntityBadge label={`${key.rateLimitPerSecond}/s`} tone="info" />
<EntityBadge label={`${key.rateLimitPerMinute}/min`} />
<EntityBadge label={`${key.requestsUsed}/${key.monthlyQuota || 0}`} />
{key.approved ? <EntityBadge label="approved" tone="success" /> : <EntityBadge label="pending" tone="warning" />}
{key.revoked ? <EntityBadge label="revoked" tone="warning" /> : <EntityBadge label="active" tone="success" />}
{key.expiresAt ? <EntityBadge label={`expires ${formatDate(key.expiresAt)}`} /> : <EntityBadge label="no expiry" />}
</div>
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
Created {formatTimestamp(key.createdAt)}
{key.lastUsedAt ? ` · Last used ${formatTimestamp(key.lastUsedAt)}` : ' · Not used yet'}
</div>
</div>
{!key.revoked ? (
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => void handleRotate(key)}
className="rounded-lg border border-primary-300 px-3 py-2 text-sm font-medium text-primary-700 hover:bg-primary-50 dark:border-primary-800 dark:text-primary-300 dark:hover:bg-primary-950/20"
>
Rotate
</button>
<button
type="button"
onClick={() => void handleRevoke(key.id)}
className="rounded-lg border border-red-300 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-300 dark:hover:bg-red-950/20"
>
Revoke
</button>
</div>
) : null}
</div>
{key.scopes.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{key.scopes.map((scope) => (
<EntityBadge key={`${key.id}-${scope}`} label={scope} className="normal-case tracking-normal" />
))}
</div>
) : null}
</div>
))}
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">No API keys issued yet.</p>
)
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">
Sign in to issue and manage RPC access keys for Core, Thirdweb, and Alltra products.
</p>
)}
<div className="mt-6 border-t border-gray-200 pt-4 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-400">
Billing, quotas, and paywalls can be layered onto this access plane next. The current slice establishes identity, product discovery, and key lifecycle management.
</div>
{user && usage.length > 0 ? (
<div className="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
<div className="mb-3 text-sm font-semibold text-gray-900 dark:text-white">Usage Summary</div>
<div className="space-y-3">
{usage.map((item) => (
<div key={item.product_slug} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-wrap items-center gap-2">
<EntityBadge label={item.product_slug} tone="info" />
<EntityBadge label={`${item.active_keys} active keys`} />
</div>
<div className="mt-2 text-gray-600 dark:text-gray-400">
{formatInteger(item.requests_used)} requests used / {formatInteger(item.monthly_quota)} monthly quota
</div>
</div>
))}
</div>
</div>
) : null}
{user ? (
<div className="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
<div className="mb-3 flex flex-wrap items-end justify-between gap-3">
<div className="text-sm font-semibold text-gray-900 dark:text-white">Recent API Activity</div>
<label className="block min-w-[10rem]">
<span className="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">Entries shown</span>
<select
value={auditLimit}
onChange={(event) => setAuditLimit(event.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
</label>
</div>
{auditEntries.length > 0 ? (
<div className="space-y-3">
{auditEntries.map((entry) => (
<div key={`audit-${entry.id}`} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-wrap items-center gap-2">
<EntityBadge label={entry.productSlug || 'unscoped'} tone="info" />
<EntityBadge label={entry.methodName || 'unknown method'} />
<EntityBadge label={`${entry.requestCount} req`} />
</div>
<div className="mt-2 font-medium text-gray-900 dark:text-white">{entry.keyName || entry.apiKeyId}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formatTimestamp(entry.createdAt)}
{entry.lastIp ? ` · ${entry.lastIp}` : ''}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">No API usage has been logged yet for this account.</p>
)}
</div>
) : null}
<div className="mt-4 flex flex-wrap gap-3 text-sm">
<Link href="/wallet" className="text-primary-600 hover:underline">Wallet </Link>
<Link href="/system" className="text-primary-600 hover:underline">System </Link>
</div>
</Card>
</div>
</TabPanel>
{user?.is_admin ? (
<TabPanel idPrefix="access" tabId="admin" activeTab={accessTab}>
<div className="space-y-4">
<Card title="Pending access review">
<div className="mb-4 flex flex-wrap items-end gap-3">
<label className="block min-w-[12rem]">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Subscription status</span>
<select
value={adminSubscriptionStatus}
onChange={(event) => setAdminSubscriptionStatus(event.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
>
<option value="pending">Pending</option>
<option value="active">Active</option>
<option value="suspended">Suspended</option>
<option value="revoked">Revoked</option>
<option value="">All statuses</option>
</select>
</label>
</div>
{adminSubscriptions.length > 0 ? (
<div className="space-y-3">
{adminSubscriptions.map((subscription) => (
<div key={subscription.id} className="rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="font-semibold text-gray-900 dark:text-white">{subscription.productSlug}</div>
<div className="mt-2 flex flex-wrap gap-2">
<EntityBadge label={subscription.status} tone={subscription.status === 'active' ? 'success' : 'warning'} />
<EntityBadge label={subscription.tier} />
<EntityBadge label={`${formatInteger(subscription.monthlyQuota)} quota`} tone="info" />
{subscription.requiresApproval ? <EntityBadge label="restricted product" tone="warning" /> : null}
</div>
<label className="mt-3 block">
<span className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-300">Admin note</span>
<input
type="text"
value={adminActionNotes[subscription.id] || ''}
onChange={(event) =>
setAdminActionNotes((current) => ({
...current,
[subscription.id]: event.target.value,
}))
}
placeholder="Reason, approval scope, or operator note"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white"
/>
</label>
</div>
<div className="flex flex-wrap gap-2">
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'active')} className="rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white hover:bg-emerald-700">Approve</button>
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'suspended')} className="rounded-lg border border-amber-300 px-3 py-2 text-sm font-medium text-amber-800 hover:bg-amber-50 dark:border-amber-800 dark:text-amber-300">Suspend</button>
<button type="button" onClick={() => void handleAdminSubscriptionAction(subscription.id, 'revoked')} className="rounded-lg border border-red-300 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-300">Revoke</button>
</div>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">No subscriptions match the current review filter.</p>
)}
</Card>
<Card title="Platform audit feed">
<div className="mb-4 grid gap-3 sm:grid-cols-2">
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Filter by product</span>
<select value={adminAuditProduct} onChange={(event) => void handleAdminAuditProductChange(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
<option value="">All products</option>
{products.map((product) => (
<option key={`audit-${product.slug}`} value={product.slug}>{product.name}</option>
))}
</select>
</label>
<label className="block">
<span className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Entries shown</span>
<select value={adminAuditLimit} onChange={(event) => setAdminAuditLimit(event.target.value)} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 dark:text-white">
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</label>
</div>
{adminAuditEntries.length > 0 ? (
<div className="space-y-3">
{adminAuditEntries.map((entry) => (
<div key={`admin-audit-${entry.id}`} className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm dark:border-gray-700 dark:bg-gray-900/40">
<div className="flex flex-wrap items-center gap-2">
<EntityBadge label={entry.productSlug || 'unscoped'} tone="info" />
<EntityBadge label={entry.methodName || 'unknown method'} />
<EntityBadge label={`${entry.requestCount} req`} />
</div>
<div className="mt-2 font-medium text-gray-900 dark:text-white">{entry.keyName || entry.apiKeyId}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{formatTimestamp(entry.createdAt)}
{entry.lastIp ? ` · ${entry.lastIp}` : ''}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">No recent validated RPC traffic matches the current filter.</p>
)}
</Card>
</div>
</TabPanel>
) : null}
</main>
)
}