| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- <!DOCTYPE html>
- <html lang="en" class="dark">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Raft KV Admin</title>
- <script src="https://cdn.tailwindcss.com"></script>
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
- <script>
- tailwind.config = {
- darkMode: 'class',
- theme: {
- extend: {
- colors: {
- dark: {
- 900: '#0f172a',
- 800: '#1e293b',
- 700: '#334155',
- },
- brand: {
- 500: '#3b82f6',
- 600: '#2563eb',
- }
- }
- }
- }
- }
- </script>
- <style>
- [x-cloak] { display: none !important; }
- </style>
- </head>
- <body class="bg-dark-900 text-slate-300 font-sans antialiased" x-data="app()">
- <!-- Login Modal -->
- <div x-show="!isLoggedIn" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
- <div class="bg-dark-800 p-8 rounded-2xl shadow-2xl w-full max-w-md border border-dark-700">
- <h2 class="text-2xl font-bold text-white mb-6 text-center">Raft KV Admin</h2>
- <form @submit.prevent="login">
- <div class="mb-4">
- <label class="block text-sm font-medium mb-2">Username</label>
- <input type="text" x-model="loginForm.user" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
- </div>
- <div class="mb-4">
- <label class="block text-sm font-medium mb-2">Password</label>
- <input type="password" x-model="loginForm.pass" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
- </div>
- <div class="mb-6">
- <label class="block text-sm font-medium mb-2">OTP (Optional)</label>
- <input type="text" x-model="loginForm.otp" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white focus:ring-2 focus:ring-brand-500 outline-none">
- </div>
- <button type="submit" class="w-full bg-brand-600 hover:bg-brand-500 text-white font-bold py-3 rounded-lg transition shadow-lg shadow-brand-500/20">
- <span x-show="!loading">Login</span>
- <span x-show="loading">Authenticating...</span>
- </button>
- <p x-show="errorMsg" x-text="errorMsg" class="mt-4 text-red-400 text-center text-sm"></p>
- </form>
- </div>
- </div>
- <!-- Main Layout -->
- <div x-show="isLoggedIn" x-cloak class="flex h-screen overflow-hidden">
-
- <!-- Sidebar -->
- <aside class="w-64 bg-dark-800 border-r border-dark-700 flex flex-col">
- <div class="p-6 flex items-center gap-3">
- <div class="w-8 h-8 rounded bg-brand-500 flex items-center justify-center font-bold text-white">R</div>
- <span class="text-xl font-bold text-white tracking-tight">Raft Admin</span>
- </div>
- <nav class="flex-1 px-4 space-y-2 mt-4">
- <a @click="tab = 'dashboard'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'dashboard', 'hover:bg-dark-700': tab !== 'dashboard'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
- Dashboard
- </a>
- <a @click="tab = 'data'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'data', 'hover:bg-dark-700': tab !== 'data'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
- KV Explorer
- </a>
- <div class="pt-4 pb-2 px-2 text-xs font-bold text-slate-500 uppercase">Access Control</div>
- <a @click="fetchUsers(); tab = 'users'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'users', 'hover:bg-dark-700': tab !== 'users'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
- Users
- </a>
- <a @click="fetchRoles(); tab = 'roles'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'roles', 'hover:bg-dark-700': tab !== 'roles'}" class="flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer transition-all">
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
- Roles
- </a>
- </nav>
- <div class="p-4 border-t border-dark-700">
- <button @click="logout" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
- Logout
- </button>
- </div>
- </aside>
- <!-- Main Content -->
- <main class="flex-1 flex flex-col overflow-hidden bg-dark-900">
- <!-- Topbar -->
- <header class="h-16 bg-dark-800/50 backdrop-blur border-b border-dark-700 flex items-center justify-between px-8">
- <h2 class="text-xl font-semibold text-white capitalize" x-text="tab"></h2>
- <div class="flex items-center gap-4">
- <span class="text-sm text-slate-400">Connected as <span class="text-white font-medium" x-text="username"></span></span>
- <div class="w-8 h-8 rounded-full bg-gradient-to-r from-brand-500 to-purple-500"></div>
- </div>
- </header>
- <!-- Scrollable Area -->
- <div class="flex-1 overflow-y-auto p-8">
-
- <!-- Dashboard Tab -->
- <div x-show="tab === 'dashboard'" class="space-y-6">
-
- <!-- System Info Grid -->
- <template x-if="info">
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
- <!-- Node Status -->
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
- <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Node Status</h4>
- <div class="space-y-2">
- <div class="flex justify-between">
- <span class="text-slate-500">ID</span>
- <span class="text-white font-mono" x-text="info.node.id"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">State</span>
- <span class="font-bold" :class="info.node.state === 'Leader' ? 'text-yellow-400' : 'text-blue-400'" x-text="info.node.state"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Term</span>
- <span class="text-white font-mono" x-text="info.node.term"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Health</span>
- <span class="" :class="info.node.healthy ? 'text-green-400' : 'text-red-400'" x-text="info.node.healthy ? 'Healthy' : 'Unhealthy'"></span>
- </div>
- </div>
- </div>
- <!-- Storage -->
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
- <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Storage</h4>
- <div class="space-y-2">
- <div class="flex justify-between">
- <span class="text-slate-500">DB Size</span>
- <span class="text-white font-mono" x-text="(info.storage.db_size / 1024).toFixed(2) + ' KB'"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Log Size</span>
- <span class="text-white font-mono" x-text="(info.storage.log_size / 1024).toFixed(2) + ' KB'"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Mem Alloc</span>
- <span class="text-white font-mono" x-text="(info.storage.mem_alloc / 1024 / 1024).toFixed(2) + ' MB'"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">GC Count</span>
- <span class="text-white font-mono" x-text="info.storage.num_gc"></span>
- </div>
- </div>
- </div>
- <!-- Consensus -->
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
- <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Consensus</h4>
- <div class="space-y-2">
- <div class="flex justify-between">
- <span class="text-slate-500">Commit Idx</span>
- <span class="text-white font-mono" x-text="info.indices.commit_index"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Applied Idx</span>
- <span class="text-white font-mono" x-text="info.indices.applied_index"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">Last Log</span>
- <span class="text-white font-mono" x-text="info.indices.last_log_index"></span>
- </div>
- <div class="flex justify-between">
- <span class="text-slate-500">DB Applied</span>
- <span class="text-white font-mono" x-text="info.indices.db_applied"></span>
- </div>
- </div>
- </div>
- <!-- Cluster -->
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 overflow-hidden">
- <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Cluster (<span x-text="info.cluster_size"></span>)</h4>
- <div class="space-y-2 max-h-32 overflow-y-auto">
- <template x-for="(addr, id) in info.cluster" :key="id">
- <div class="flex justify-between items-center text-sm">
- <div class="flex items-center gap-2">
- <div class="w-2 h-2 rounded-full" :class="id === info.node.leader ? 'bg-yellow-400' : 'bg-green-400'"></div>
- <span class="text-white font-mono" x-text="id"></span>
- </div>
- <span class="text-slate-500 text-xs" x-text="addr"></span>
- </div>
- </template>
- </div>
- </div>
- </div>
- </template>
- <!-- Loading State -->
- <template x-if="!info">
- <div class="text-center p-12 bg-dark-800 rounded-2xl border border-dark-700 animate-pulse">
- <div class="text-slate-500">Loading Node Info...</div>
- </div>
- </template>
-
- <div class="bg-dark-800 rounded-2xl border border-dark-700 p-6">
- <h3 class="text-lg font-bold text-white mb-4">Web CLI</h3>
- <div class="mb-4 text-sm text-slate-400 bg-dark-900/50 p-3 rounded-lg border border-dark-700 font-mono leading-relaxed max-h-40 overflow-y-auto">
- <div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
- <div>
- <div class="text-xs font-bold text-slate-500 mb-1 uppercase">KV Data</div>
- <div class="space-y-1">
- <div><span class="text-brand-500">GET</span> <key></div>
- <div><span class="text-brand-500">SET</span> <key> <val></div>
- <div><span class="text-brand-500">DEL</span> <key></div>
- <div><span class="text-brand-500">SEARCH</span> <pat> [lim]</div>
- <div><span class="text-brand-500">COUNT</span> <pat></div>
- </div>
- </div>
- <div>
- <div class="text-xs font-bold text-slate-500 mb-1 uppercase">Cluster</div>
- <div class="space-y-1">
- <div><span class="text-brand-500">INFO</span></div>
- <div><span class="text-brand-500">JOIN</span> <id> <addr></div>
- <div><span class="text-brand-500">LEAVE</span> <id></div>
- </div>
- </div>
- <div class="md:col-span-2 mt-2">
- <div class="text-xs font-bold text-slate-500 mb-1 uppercase">User & Role Management</div>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1">
- <div><span class="text-brand-500">USER_LIST</span></div>
- <div><span class="text-brand-500">ROLE_LIST</span></div>
- <div><span class="text-brand-500">WHOAMI</span></div>
- <div><span class="text-brand-500">USER_CREATE</span> <u> <p> [roles]</div>
- <div><span class="text-brand-500">ROLE_CREATE</span> <name></div>
- <div><span class="text-brand-500">USER_UNLOCK</span> <u></div>
- <div class="md:col-span-2"><span class="text-brand-500">ROLE_PERMISSION_ADD</span> <role> <pat> <acts></div>
- </div>
- </div>
- </div>
- </div>
- <div class="flex gap-2">
- <input type="text" x-model="quickCmd" @keyup.enter="runQuickCmd" placeholder="Type command (e.g. HELP)..." class="flex-1 bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500 font-mono">
- <button @click="runQuickCmd" class="bg-brand-600 px-6 rounded-lg text-white font-bold hover:bg-brand-500">Run</button>
- </div>
- <div x-show="quickResult" class="mt-4 bg-black/50 p-4 rounded-lg border border-dark-700 overflow-x-auto">
- <pre class="text-sm text-green-400 font-mono whitespace-pre-wrap" x-text="quickResult"></pre>
- </div>
- </div>
- </div>
- <!-- KV Explorer Tab -->
- <div x-show="tab === 'data'" class="space-y-6">
- <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
- <div class="p-6 border-b border-dark-700 flex justify-between items-center">
- <h3 class="text-lg font-bold text-white">Key-Value Operations</h3>
- </div>
- <div class="p-6 space-y-4">
- <!-- Operation Form -->
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
- <select x-model="kvOp" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <option value="GET">GET</option>
- <option value="SET">SET</option>
- <option value="DEL">DEL</option>
- </select>
- <input x-model="kvKey" type="text" placeholder="Key" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <input x-show="kvOp === 'SET'" x-model="kvVal" type="text" placeholder="Value" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white md:col-span-1">
- <button @click="executeKV" class="bg-brand-600 text-white rounded-lg p-3 font-bold hover:bg-brand-500">Execute</button>
- </div>
-
- <!-- Result Display -->
- <div x-show="kvResult" class="mt-4 p-4 rounded-lg border border-dark-700" :class="kvError ? 'bg-red-900/20 border-red-800 text-red-200' : 'bg-green-900/20 border-green-800 text-green-200'">
- <span class="font-bold block mb-1">Result:</span>
- <span class="font-mono" x-text="kvResult"></span>
- </div>
- </div>
- </div>
- </div>
- <!-- Users Tab -->
- <div x-show="tab === 'users'" class="space-y-6">
- <!-- Actions -->
- <div class="flex justify-end">
- <button @click="showAddUserModal = true" class="bg-brand-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-brand-500 flex items-center gap-2">
- <span>+ Add User</span>
- </button>
- </div>
- <!-- User Table -->
- <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
- <table class="w-full text-left">
- <thead class="bg-dark-700 text-slate-300">
- <tr>
- <th class="p-4">Username</th>
- <th class="p-4">Roles</th>
- <th class="p-4">Status</th>
- <th class="p-4 text-right">Actions</th>
- </tr>
- </thead>
- <tbody class="divide-y divide-dark-700">
- <template x-for="u in users" :key="u.username">
- <tr class="hover:bg-dark-700/50 transition">
- <td class="p-4 font-medium text-white" x-text="u.username"></td>
- <td class="p-4">
- <div class="flex gap-2">
- <template x-for="r in (u.roles || [])">
- <span class="px-2 py-1 bg-blue-500/20 text-blue-300 rounded text-xs" x-text="r"></span>
- </template>
- </div>
- </td>
- <td class="p-4">
- <span class="inline-flex items-center gap-1 text-green-400 text-sm">
- <span class="w-2 h-2 rounded-full bg-green-400"></span> Active
- </span>
- </td>
- <td class="p-4 text-right">
- <button @click="unlockUser(u.username)" class="text-yellow-500 hover:text-yellow-400 text-sm mr-3">Reset Lock</button>
- <!-- <button class="text-red-500 hover:text-red-400 text-sm">Delete</button> -->
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </div>
- </div>
- <!-- Roles Tab -->
- <div x-show="tab === 'roles'" class="space-y-6">
- <div class="flex justify-end">
- <button @click="showAddRoleModal = true" class="bg-brand-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-brand-500">
- + Create Role
- </button>
- </div>
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
- <template x-for="r in roles" :key="r.name">
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 hover:border-brand-500 transition group">
- <div class="flex justify-between items-start mb-4">
- <h3 class="text-lg font-bold text-white" x-text="r.name"></h3>
- <span class="text-xs text-slate-500 bg-dark-900 px-2 py-1 rounded" x-text="(r.permissions || []).length + ' Rules'"></span>
- </div>
- <div class="space-y-2 mb-4">
- <template x-for="p in (r.permissions || []).slice(0, 3)">
- <div class="text-sm text-slate-400 font-mono bg-dark-900/50 p-1 rounded px-2">
- <span x-text="p.key"></span>
- <span class="text-brand-500" x-text="'[' + p.actions.join(',') + ']'"></span>
- </div>
- </template>
- <div x-show="(r.permissions || []).length > 3" class="text-xs text-slate-500 italic">...and more</div>
- </div>
- <button @click="openAddPermModal(r.name)" class="w-full py-2 bg-dark-700 hover:bg-dark-600 text-sm text-white rounded-lg transition">
- Add Permission
- </button>
- </div>
- </template>
- </div>
- </div>
- </div>
- </main>
- </div>
- <!-- Modals -->
- <!-- Add User Modal -->
- <div x-show="showAddUserModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
- <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
- <h3 class="text-xl font-bold text-white mb-4">Create New User</h3>
- <div class="space-y-4">
- <input x-model="newUser.username" type="text" placeholder="Username" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <input x-model="newUser.password" type="password" placeholder="Password" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <input x-model="newUser.roles" type="text" placeholder="Roles (comma separated)" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <div class="flex gap-3 pt-2">
- <button @click="createUser" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
- <button @click="showAddUserModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
- </div>
- </div>
- </div>
- </div>
- <!-- Add Role Modal -->
- <div x-show="showAddRoleModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
- <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
- <h3 class="text-xl font-bold text-white mb-4">Create New Role</h3>
- <div class="space-y-4">
- <input x-model="newRoleName" type="text" placeholder="Role Name" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
- <div class="flex gap-3 pt-2">
- <button @click="createRole" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
- <button @click="showAddRoleModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
- </div>
- </div>
- </div>
- </div>
- <!-- Add Permission Modal -->
- <div x-show="showAddPermModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
- <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
- <h3 class="text-xl font-bold text-white mb-4">Add Permission to <span x-text="selectedRole" class="text-brand-500"></span></h3>
- <div class="space-y-4">
- <div>
- <label class="block text-sm font-medium mb-2 text-slate-400">Key Pattern</label>
- <input x-model="newPerm.pattern" type="text" placeholder="e.g. user.* or *" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500">
- </div>
- <div>
- <label class="block text-sm font-medium mb-2 text-slate-400">Actions</label>
- <div class="flex gap-4 p-3 bg-dark-900 rounded-lg border border-dark-700">
- <label class="flex items-center gap-2 text-white cursor-pointer">
- <input type="checkbox" value="read" x-model="newPerm.actions" class="accent-brand-500"> Read
- </label>
- <label class="flex items-center gap-2 text-white cursor-pointer">
- <input type="checkbox" value="write" x-model="newPerm.actions" class="accent-brand-500"> Write
- </label>
- <label class="flex items-center gap-2 text-white cursor-pointer">
- <input type="checkbox" value="admin" x-model="newPerm.actions" class="accent-brand-500"> Admin
- </label>
- </div>
- </div>
- <div class="flex gap-3 pt-2">
- <button @click="addPermission" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Add</button>
- <button @click="showAddPermModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
- </div>
- </div>
- </div>
- </div>
- <script>
- function app() {
- return {
- isLoggedIn: false,
- token: localStorage.getItem('raft_token') || '',
- username: localStorage.getItem('raft_user') || '',
- tab: 'dashboard',
- loading: false,
- errorMsg: '',
-
- // Login Form
- loginForm: { user: '', pass: '', otp: '' },
- // Data
- users: [],
- roles: [],
- kvHistory: [], // Track KV ops
- info: null, // System Info
- // KV Op
- kvOp: 'GET',
- kvKey: '',
- kvVal: '',
- kvResult: '',
- kvError: false,
- // Quick Command
- quickCmd: '',
- quickResult: '',
- // Modals
- showAddUserModal: false,
- newUser: { username: '', password: '', roles: '' },
- showAddRoleModal: false,
- newRoleName: '',
- // Add Permission Modal
- showAddPermModal: false,
- selectedRole: '',
- newPerm: { pattern: '', actions: [] },
- init() {
- if (this.token) {
- this.isLoggedIn = true;
- this.fetchUsers();
- this.fetchInfo();
- // Refresh info periodically
- setInterval(() => {
- if (this.isLoggedIn && this.tab === 'dashboard') {
- this.fetchInfo();
- }
- }, 2000);
- }
- },
- async api(command, args = []) {
- try {
- const res = await fetch('/api/proxy', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Session-Token': this.token
- },
- body: JSON.stringify({ command, args })
- });
- const json = await res.json();
- if (json.status === 'error') {
- if (json.error === 'Session Expired' || json.error.startsWith('Connection failed')) {
- this.logout();
- }
- throw new Error(json.error);
- }
- return json.data;
- } catch (e) {
- throw e;
- }
- },
- async login() {
- this.loading = true;
- this.errorMsg = '';
- try {
- // Args: user pass [otp]
- const args = [this.loginForm.user, this.loginForm.pass];
- if (this.loginForm.otp) args.push(this.loginForm.otp);
- const token = await this.api('LOGIN', args);
-
- // If success, token is returned string
- // Note: My TCP API returns "OK <token>" string or JSON if modified.
- // The proxy handles "OK " stripping. So 'token' variable here holds the raw token string.
-
- this.token = token.trim();
- this.username = this.loginForm.user;
- localStorage.setItem('raft_token', this.token);
- localStorage.setItem('raft_user', this.username);
- this.isLoggedIn = true;
- this.fetchUsers();
- } catch (e) {
- this.errorMsg = e.message;
- } finally {
- this.loading = false;
- }
- },
- logout() {
- this.api('LOGOUT');
- this.token = '';
- this.username = '';
- this.isLoggedIn = false;
- localStorage.removeItem('raft_token');
- },
- async fetchUsers() {
- try {
- this.users = await this.api('USER_LIST');
- if (typeof this.users === 'string') {
- this.users = JSON.parse(this.users);
- }
- } catch (e) {
- console.error("Failed to fetch users", e);
- }
- },
- async fetchInfo() {
- try {
- const res = await this.api('INFO');
- // TCP returns "OK {json}" or just {json} via proxy parsing?
- // api() helper returns json.data.
- // If TCP returns "OK <json>", proxy parses it if it looks like JSON.
- // Our INFO returns "OK <json>".
- this.info = res;
- if (typeof this.info === 'string') {
- try {
- this.info = JSON.parse(this.info);
- } catch(e) { /* ignore */ }
- }
- } catch (e) {
- console.error("Failed to fetch info", e);
- }
- },
- async fetchRoles() {
- try {
- this.roles = await this.api('ROLE_LIST');
- } catch (e) {
- console.error(e);
- if (e.message.includes("unknown command")) {
- alert("Server error: ROLE_LIST command not found. Please restart your Raft node to apply latest changes.");
- }
- }
- },
- async executeKV() {
- this.kvResult = 'Processing...';
- this.kvError = false;
- try {
- const args = [this.kvKey];
- if (this.kvOp === 'SET') args.push(this.kvVal);
-
- const res = await this.api(this.kvOp, args);
- this.kvResult = typeof res === 'object' ? JSON.stringify(res) : res;
- this.kvHistory.push({ op: this.kvOp, key: this.kvKey, time: new Date().toLocaleTimeString() });
- } catch (e) {
- this.kvError = true;
- this.kvResult = e.message;
- }
- },
- async runQuickCmd() {
- if(!this.quickCmd) return;
- const parts = this.quickCmd.split(' ');
- const cmd = parts[0].toUpperCase();
- const args = parts.slice(1);
- try {
- const res = await this.api(cmd, args);
- this.quickResult = typeof res === 'object' ? JSON.stringify(res, null, 2) : res;
- } catch (e) {
- this.quickResult = "Error: " + e.message;
- }
- },
- async createUser() {
- try {
- await this.api('USER_CREATE', [this.newUser.username, this.newUser.password, this.newUser.roles]);
- this.showAddUserModal = false;
- this.newUser = { username: '', password: '', roles: '' };
- this.fetchUsers();
- } catch (e) {
- alert(e.message);
- }
- },
- async createRole() {
- try {
- await this.api('ROLE_CREATE', [this.newRoleName]);
- this.showAddRoleModal = false;
- this.newRoleName = '';
- this.fetchRoles();
- } catch (e) {
- alert(e.message);
- }
- },
- openAddPermModal(roleName) {
- this.selectedRole = roleName;
- this.newPerm = { pattern: '', actions: [] };
- this.showAddPermModal = true;
- },
- async addPermission() {
- if (!this.newPerm.pattern || this.newPerm.actions.length === 0) {
- alert("Please provide pattern and at least one action");
- return;
- }
- try {
- // Command: ROLE_PERMISSION_ADD <role> <pattern> <actions>
- const actionsStr = this.newPerm.actions.join(',');
- await this.api('ROLE_PERMISSION_ADD', [this.selectedRole, this.newPerm.pattern, actionsStr]);
- this.showAddPermModal = false;
- this.fetchRoles();
- } catch (e) {
- alert(e.message);
- }
- },
- async unlockUser(username) {
- if(!confirm("Unlock user " + username + "?")) return;
- try {
- await this.api('USER_UNLOCK', [username]);
- alert("Unlocked");
- } catch (e) { alert(e.message); }
- }
- }
- }
- </script>
- </body>
- </html>
|