refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions

View File

@@ -0,0 +1,873 @@
import { FormEvent, useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives'
import PageIntro from '@/components/common/PageIntro'
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'
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 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-6 sm:py-8">
<PageIntro
eyebrow="Access Control"
title="Wallet Login, RPC Access & API Tokens"
description="Connect a wallet for standard account sign-in, manage authenticated access, issue API keys, and prepare subscription-gated RPC products for DBIS, ThirdWeb, and Alltra."
actions={[
{ href: '/wallet', label: 'Wallet tools' },
{ href: '/system', label: 'System status' },
{ href: '/search', label: 'Search explorer' },
]}
/>
{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}
<div className="mb-8 grid gap-6 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>
<div className="grid gap-6 lg:grid-cols-[1fr_1.2fr]">
<div className="space-y-6">
<Card title="Wallet Authentication">
<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 {new Date(walletSession.expiresAt).toLocaleString()}
</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>
<Card title="Operator Identities">
<div className="space-y-4">
{OPERATOR_IDENTITIES.map((identity) => (
<div key={identity.slug} 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-wrap gap-2">
<EntityBadge label={identity.label} tone="info" />
<EntityBadge label={identity.slug} />
<EntityBadge label={`vmid ${identity.vmid}`} tone="warning" />
</div>
<code className="mt-3 block break-all text-xs text-gray-700 dark:text-gray-300">{identity.address}</code>
</div>
))}
</div>
</Card>
<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}
{user?.is_admin ? (
<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-4">
{adminSubscriptions.map((subscription) => (
<div key={subscription.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">{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={`${subscription.monthlyQuota.toLocaleString()} quota`} tone="info" />
{subscription.requiresApproval ? <EntityBadge label="restricted product" tone="warning" /> : null}
</div>
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
Requested {new Date(subscription.createdAt).toLocaleString()}
{subscription.notes ? ` · ${subscription.notes}` : ''}
</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 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>
</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 dark:hover:bg-amber-950/20"
>
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 dark:hover:bg-red-950/20"
>
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>
) : null}
{user?.is_admin ? (
<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">
{new Date(entry.createdAt).toLocaleString()}
{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>
) : 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 ${new Date(key.expiresAt).toLocaleDateString()}`} /> : <EntityBadge label="no expiry" />}
</div>
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
Created {new Date(key.createdAt).toLocaleString()}
{key.lastUsedAt ? ` · Last used ${new Date(key.lastUsedAt).toLocaleString()}` : ' · 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">
{item.requests_used.toLocaleString()} requests used / {item.monthly_quota.toLocaleString()} 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">
{new Date(entry.createdAt).toLocaleString()}
{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>
</main>
)
}