index.html 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. <!DOCTYPE html>
  2. <html lang="en" class="dark">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Raft KV Admin</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
  9. <script>
  10. tailwind.config = {
  11. darkMode: 'class',
  12. theme: {
  13. extend: {
  14. colors: {
  15. dark: {
  16. 900: '#0f172a',
  17. 800: '#1e293b',
  18. 700: '#334155',
  19. },
  20. brand: {
  21. 500: '#3b82f6',
  22. 600: '#2563eb',
  23. }
  24. }
  25. }
  26. }
  27. }
  28. </script>
  29. <style>
  30. [x-cloak] { display: none !important; }
  31. </style>
  32. </head>
  33. <body class="bg-dark-900 text-slate-300 font-sans antialiased" x-data="app()">
  34. <!-- Login Modal -->
  35. <div x-show="!isLoggedIn" x-transition.opacity class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
  36. <div class="bg-dark-800 p-8 rounded-2xl shadow-2xl w-full max-w-md border border-dark-700">
  37. <h2 class="text-2xl font-bold text-white mb-6 text-center">Raft KV Admin</h2>
  38. <form @submit.prevent="login">
  39. <div class="mb-4">
  40. <label class="block text-sm font-medium mb-2">Username</label>
  41. <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">
  42. </div>
  43. <div class="mb-4">
  44. <label class="block text-sm font-medium mb-2">Password</label>
  45. <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">
  46. </div>
  47. <div class="mb-6">
  48. <label class="block text-sm font-medium mb-2">OTP (Optional)</label>
  49. <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">
  50. </div>
  51. <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">
  52. <span x-show="!loading">Login</span>
  53. <span x-show="loading">Authenticating...</span>
  54. </button>
  55. <p x-show="errorMsg" x-text="errorMsg" class="mt-4 text-red-400 text-center text-sm"></p>
  56. </form>
  57. </div>
  58. </div>
  59. <!-- Main Layout -->
  60. <div x-show="isLoggedIn" x-cloak class="flex h-screen overflow-hidden">
  61. <!-- Sidebar -->
  62. <aside class="w-64 bg-dark-800 border-r border-dark-700 flex flex-col">
  63. <div class="p-6 flex items-center gap-3">
  64. <div class="w-8 h-8 rounded bg-brand-500 flex items-center justify-center font-bold text-white">R</div>
  65. <span class="text-xl font-bold text-white tracking-tight">Raft Admin</span>
  66. </div>
  67. <nav class="flex-1 px-4 space-y-2 mt-4">
  68. <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">
  69. <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>
  70. Dashboard
  71. </a>
  72. <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">
  73. <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>
  74. KV Explorer
  75. </a>
  76. <div class="pt-4 pb-2 px-2 text-xs font-bold text-slate-500 uppercase">Access Control</div>
  77. <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">
  78. <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>
  79. Users
  80. </a>
  81. <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">
  82. <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>
  83. Roles
  84. </a>
  85. </nav>
  86. <div class="p-4 border-t border-dark-700">
  87. <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">
  88. <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>
  89. Logout
  90. </button>
  91. </div>
  92. </aside>
  93. <!-- Main Content -->
  94. <main class="flex-1 flex flex-col overflow-hidden bg-dark-900">
  95. <!-- Topbar -->
  96. <header class="h-16 bg-dark-800/50 backdrop-blur border-b border-dark-700 flex items-center justify-between px-8">
  97. <h2 class="text-xl font-semibold text-white capitalize" x-text="tab"></h2>
  98. <div class="flex items-center gap-4">
  99. <span class="text-sm text-slate-400">Connected as <span class="text-white font-medium" x-text="username"></span></span>
  100. <div class="w-8 h-8 rounded-full bg-gradient-to-r from-brand-500 to-purple-500"></div>
  101. </div>
  102. </header>
  103. <!-- Scrollable Area -->
  104. <div class="flex-1 overflow-y-auto p-8">
  105. <!-- Dashboard Tab -->
  106. <div x-show="tab === 'dashboard'" class="space-y-6">
  107. <!-- System Info Grid -->
  108. <template x-if="info">
  109. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
  110. <!-- Node Status -->
  111. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  112. <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Node Status</h4>
  113. <div class="space-y-2">
  114. <div class="flex justify-between">
  115. <span class="text-slate-500">ID</span>
  116. <span class="text-white font-mono" x-text="info.node.id"></span>
  117. </div>
  118. <div class="flex justify-between">
  119. <span class="text-slate-500">State</span>
  120. <span class="font-bold" :class="info.node.state === 'Leader' ? 'text-yellow-400' : 'text-blue-400'" x-text="info.node.state"></span>
  121. </div>
  122. <div class="flex justify-between">
  123. <span class="text-slate-500">Term</span>
  124. <span class="text-white font-mono" x-text="info.node.term"></span>
  125. </div>
  126. <div class="flex justify-between">
  127. <span class="text-slate-500">Health</span>
  128. <span class="" :class="info.node.healthy ? 'text-green-400' : 'text-red-400'" x-text="info.node.healthy ? 'Healthy' : 'Unhealthy'"></span>
  129. </div>
  130. </div>
  131. </div>
  132. <!-- Storage -->
  133. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  134. <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Storage</h4>
  135. <div class="space-y-2">
  136. <div class="flex justify-between">
  137. <span class="text-slate-500">DB Size</span>
  138. <span class="text-white font-mono" x-text="(info.storage.db_size / 1024).toFixed(2) + ' KB'"></span>
  139. </div>
  140. <div class="flex justify-between">
  141. <span class="text-slate-500">Log Size</span>
  142. <span class="text-white font-mono" x-text="(info.storage.log_size / 1024).toFixed(2) + ' KB'"></span>
  143. </div>
  144. <div class="flex justify-between">
  145. <span class="text-slate-500">Mem Alloc</span>
  146. <span class="text-white font-mono" x-text="(info.storage.mem_alloc / 1024 / 1024).toFixed(2) + ' MB'"></span>
  147. </div>
  148. <div class="flex justify-between">
  149. <span class="text-slate-500">GC Count</span>
  150. <span class="text-white font-mono" x-text="info.storage.num_gc"></span>
  151. </div>
  152. </div>
  153. </div>
  154. <!-- Consensus -->
  155. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
  156. <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Consensus</h4>
  157. <div class="space-y-2">
  158. <div class="flex justify-between">
  159. <span class="text-slate-500">Commit Idx</span>
  160. <span class="text-white font-mono" x-text="info.indices.commit_index"></span>
  161. </div>
  162. <div class="flex justify-between">
  163. <span class="text-slate-500">Applied Idx</span>
  164. <span class="text-white font-mono" x-text="info.indices.applied_index"></span>
  165. </div>
  166. <div class="flex justify-between">
  167. <span class="text-slate-500">Last Log</span>
  168. <span class="text-white font-mono" x-text="info.indices.last_log_index"></span>
  169. </div>
  170. <div class="flex justify-between">
  171. <span class="text-slate-500">DB Applied</span>
  172. <span class="text-white font-mono" x-text="info.indices.db_applied"></span>
  173. </div>
  174. </div>
  175. </div>
  176. <!-- Cluster -->
  177. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 overflow-hidden">
  178. <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Cluster (<span x-text="info.cluster_size"></span>)</h4>
  179. <div class="space-y-2 max-h-32 overflow-y-auto">
  180. <template x-for="(addr, id) in info.cluster" :key="id">
  181. <div class="flex justify-between items-center text-sm">
  182. <div class="flex items-center gap-2">
  183. <div class="w-2 h-2 rounded-full" :class="id === info.node.leader ? 'bg-yellow-400' : 'bg-green-400'"></div>
  184. <span class="text-white font-mono" x-text="id"></span>
  185. </div>
  186. <span class="text-slate-500 text-xs" x-text="addr"></span>
  187. </div>
  188. </template>
  189. </div>
  190. </div>
  191. </div>
  192. </template>
  193. <!-- Loading State -->
  194. <template x-if="!info">
  195. <div class="text-center p-12 bg-dark-800 rounded-2xl border border-dark-700 animate-pulse">
  196. <div class="text-slate-500">Loading Node Info...</div>
  197. </div>
  198. </template>
  199. <div class="bg-dark-800 rounded-2xl border border-dark-700 p-6">
  200. <h3 class="text-lg font-bold text-white mb-4">Web CLI</h3>
  201. <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">
  202. <div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
  203. <div>
  204. <div class="text-xs font-bold text-slate-500 mb-1 uppercase">KV Data</div>
  205. <div class="space-y-1">
  206. <div><span class="text-brand-500">GET</span> &lt;key&gt;</div>
  207. <div><span class="text-brand-500">SET</span> &lt;key&gt; &lt;val&gt;</div>
  208. <div><span class="text-brand-500">DEL</span> &lt;key&gt;</div>
  209. <div><span class="text-brand-500">SEARCH</span> &lt;pat&gt; [lim]</div>
  210. <div><span class="text-brand-500">COUNT</span> &lt;pat&gt;</div>
  211. </div>
  212. </div>
  213. <div>
  214. <div class="text-xs font-bold text-slate-500 mb-1 uppercase">Cluster</div>
  215. <div class="space-y-1">
  216. <div><span class="text-brand-500">INFO</span></div>
  217. <div><span class="text-brand-500">JOIN</span> &lt;id&gt; &lt;addr&gt;</div>
  218. <div><span class="text-brand-500">LEAVE</span> &lt;id&gt;</div>
  219. </div>
  220. </div>
  221. <div class="md:col-span-2 mt-2">
  222. <div class="text-xs font-bold text-slate-500 mb-1 uppercase">User & Role Management</div>
  223. <div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1">
  224. <div><span class="text-brand-500">USER_LIST</span></div>
  225. <div><span class="text-brand-500">ROLE_LIST</span></div>
  226. <div><span class="text-brand-500">WHOAMI</span></div>
  227. <div><span class="text-brand-500">USER_CREATE</span> &lt;u&gt; &lt;p&gt; [roles]</div>
  228. <div><span class="text-brand-500">ROLE_CREATE</span> &lt;name&gt;</div>
  229. <div><span class="text-brand-500">USER_UNLOCK</span> &lt;u&gt;</div>
  230. <div class="md:col-span-2"><span class="text-brand-500">ROLE_PERMISSION_ADD</span> &lt;role&gt; &lt;pat&gt; &lt;acts&gt;</div>
  231. </div>
  232. </div>
  233. </div>
  234. </div>
  235. <div class="flex gap-2">
  236. <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">
  237. <button @click="runQuickCmd" class="bg-brand-600 px-6 rounded-lg text-white font-bold hover:bg-brand-500">Run</button>
  238. </div>
  239. <div x-show="quickResult" class="mt-4 bg-black/50 p-4 rounded-lg border border-dark-700 overflow-x-auto">
  240. <pre class="text-sm text-green-400 font-mono whitespace-pre-wrap" x-text="quickResult"></pre>
  241. </div>
  242. </div>
  243. </div>
  244. <!-- KV Explorer Tab -->
  245. <div x-show="tab === 'data'" class="space-y-6">
  246. <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
  247. <div class="p-6 border-b border-dark-700 flex justify-between items-center">
  248. <h3 class="text-lg font-bold text-white">Key-Value Operations</h3>
  249. </div>
  250. <div class="p-6 space-y-4">
  251. <!-- Operation Form -->
  252. <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
  253. <select x-model="kvOp" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  254. <option value="GET">GET</option>
  255. <option value="SET">SET</option>
  256. <option value="DEL">DEL</option>
  257. </select>
  258. <input x-model="kvKey" type="text" placeholder="Key" class="bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
  259. <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">
  260. <button @click="executeKV" class="bg-brand-600 text-white rounded-lg p-3 font-bold hover:bg-brand-500">Execute</button>
  261. </div>
  262. <!-- Result Display -->
  263. <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'">
  264. <span class="font-bold block mb-1">Result:</span>
  265. <span class="font-mono" x-text="kvResult"></span>
  266. </div>
  267. </div>
  268. </div>
  269. </div>
  270. <!-- Users Tab -->
  271. <div x-show="tab === 'users'" class="space-y-6">
  272. <!-- Actions -->
  273. <div class="flex justify-end">
  274. <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">
  275. <span>+ Add User</span>
  276. </button>
  277. </div>
  278. <!-- User Table -->
  279. <div class="bg-dark-800 rounded-2xl border border-dark-700 overflow-hidden">
  280. <table class="w-full text-left">
  281. <thead class="bg-dark-700 text-slate-300">
  282. <tr>
  283. <th class="p-4">Username</th>
  284. <th class="p-4">Roles</th>
  285. <th class="p-4">Status</th>
  286. <th class="p-4 text-right">Actions</th>
  287. </tr>
  288. </thead>
  289. <tbody class="divide-y divide-dark-700">
  290. <template x-for="u in users" :key="u.username">
  291. <tr class="hover:bg-dark-700/50 transition">
  292. <td class="p-4 font-medium text-white" x-text="u.username"></td>
  293. <td class="p-4">
  294. <div class="flex gap-2">
  295. <template x-for="r in (u.roles || [])">
  296. <span class="px-2 py-1 bg-blue-500/20 text-blue-300 rounded text-xs" x-text="r"></span>
  297. </template>
  298. </div>
  299. </td>
  300. <td class="p-4">
  301. <span class="inline-flex items-center gap-1 text-green-400 text-sm">
  302. <span class="w-2 h-2 rounded-full bg-green-400"></span> Active
  303. </span>
  304. </td>
  305. <td class="p-4 text-right">
  306. <button @click="unlockUser(u.username)" class="text-yellow-500 hover:text-yellow-400 text-sm mr-3">Reset Lock</button>
  307. <!-- <button class="text-red-500 hover:text-red-400 text-sm">Delete</button> -->
  308. </td>
  309. </tr>
  310. </template>
  311. </tbody>
  312. </table>
  313. </div>
  314. </div>
  315. <!-- Roles Tab -->
  316. <div x-show="tab === 'roles'" class="space-y-6">
  317. <div class="flex justify-end">
  318. <button @click="showAddRoleModal = true" class="bg-brand-600 text-white px-4 py-2 rounded-lg font-bold hover:bg-brand-500">
  319. + Create Role
  320. </button>
  321. </div>
  322. <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  323. <template x-for="r in roles" :key="r.name">
  324. <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 hover:border-brand-500 transition group">
  325. <div class="flex justify-between items-start mb-4">
  326. <h3 class="text-lg font-bold text-white" x-text="r.name"></h3>
  327. <span class="text-xs text-slate-500 bg-dark-900 px-2 py-1 rounded" x-text="(r.permissions || []).length + ' Rules'"></span>
  328. </div>
  329. <div class="space-y-2 mb-4">
  330. <template x-for="p in (r.permissions || []).slice(0, 3)">
  331. <div class="text-sm text-slate-400 font-mono bg-dark-900/50 p-1 rounded px-2">
  332. <span x-text="p.key"></span>
  333. <span class="text-brand-500" x-text="'[' + p.actions.join(',') + ']'"></span>
  334. </div>
  335. </template>
  336. <div x-show="(r.permissions || []).length > 3" class="text-xs text-slate-500 italic">...and more</div>
  337. </div>
  338. <button @click="openAddPermModal(r.name)" class="w-full py-2 bg-dark-700 hover:bg-dark-600 text-sm text-white rounded-lg transition">
  339. Add Permission
  340. </button>
  341. </div>
  342. </template>
  343. </div>
  344. </div>
  345. </div>
  346. </main>
  347. </div>
  348. <!-- Modals -->
  349. <!-- Add User Modal -->
  350. <div x-show="showAddUserModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  351. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  352. <h3 class="text-xl font-bold text-white mb-4">Create New User</h3>
  353. <div class="space-y-4">
  354. <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">
  355. <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">
  356. <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">
  357. <div class="flex gap-3 pt-2">
  358. <button @click="createUser" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  359. <button @click="showAddUserModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  360. </div>
  361. </div>
  362. </div>
  363. </div>
  364. <!-- Add Role Modal -->
  365. <div x-show="showAddRoleModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  366. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  367. <h3 class="text-xl font-bold text-white mb-4">Create New Role</h3>
  368. <div class="space-y-4">
  369. <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">
  370. <div class="flex gap-3 pt-2">
  371. <button @click="createRole" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  372. <button @click="showAddRoleModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  373. </div>
  374. </div>
  375. </div>
  376. </div>
  377. <!-- Add Permission Modal -->
  378. <div x-show="showAddPermModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  379. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  380. <h3 class="text-xl font-bold text-white mb-4">Add Permission to <span x-text="selectedRole" class="text-brand-500"></span></h3>
  381. <div class="space-y-4">
  382. <div>
  383. <label class="block text-sm font-medium mb-2 text-slate-400">Key Pattern</label>
  384. <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">
  385. </div>
  386. <div>
  387. <label class="block text-sm font-medium mb-2 text-slate-400">Actions</label>
  388. <div class="flex gap-4 p-3 bg-dark-900 rounded-lg border border-dark-700">
  389. <label class="flex items-center gap-2 text-white cursor-pointer">
  390. <input type="checkbox" value="read" x-model="newPerm.actions" class="accent-brand-500"> Read
  391. </label>
  392. <label class="flex items-center gap-2 text-white cursor-pointer">
  393. <input type="checkbox" value="write" x-model="newPerm.actions" class="accent-brand-500"> Write
  394. </label>
  395. <label class="flex items-center gap-2 text-white cursor-pointer">
  396. <input type="checkbox" value="admin" x-model="newPerm.actions" class="accent-brand-500"> Admin
  397. </label>
  398. </div>
  399. </div>
  400. <div class="flex gap-3 pt-2">
  401. <button @click="addPermission" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Add</button>
  402. <button @click="showAddPermModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  403. </div>
  404. </div>
  405. </div>
  406. </div>
  407. <script>
  408. function app() {
  409. return {
  410. isLoggedIn: false,
  411. token: localStorage.getItem('raft_token') || '',
  412. username: localStorage.getItem('raft_user') || '',
  413. tab: 'dashboard',
  414. loading: false,
  415. errorMsg: '',
  416. // Login Form
  417. loginForm: { user: '', pass: '', otp: '' },
  418. // Data
  419. users: [],
  420. roles: [],
  421. kvHistory: [], // Track KV ops
  422. info: null, // System Info
  423. // KV Op
  424. kvOp: 'GET',
  425. kvKey: '',
  426. kvVal: '',
  427. kvResult: '',
  428. kvError: false,
  429. // Quick Command
  430. quickCmd: '',
  431. quickResult: '',
  432. // Modals
  433. showAddUserModal: false,
  434. newUser: { username: '', password: '', roles: '' },
  435. showAddRoleModal: false,
  436. newRoleName: '',
  437. // Add Permission Modal
  438. showAddPermModal: false,
  439. selectedRole: '',
  440. newPerm: { pattern: '', actions: [] },
  441. init() {
  442. if (this.token) {
  443. this.isLoggedIn = true;
  444. this.fetchUsers();
  445. this.fetchInfo();
  446. // Refresh info periodically
  447. setInterval(() => {
  448. if (this.isLoggedIn && this.tab === 'dashboard') {
  449. this.fetchInfo();
  450. }
  451. }, 2000);
  452. }
  453. },
  454. async api(command, args = []) {
  455. try {
  456. const res = await fetch('/api/proxy', {
  457. method: 'POST',
  458. headers: {
  459. 'Content-Type': 'application/json',
  460. 'X-Session-Token': this.token
  461. },
  462. body: JSON.stringify({ command, args })
  463. });
  464. const json = await res.json();
  465. if (json.status === 'error') {
  466. if (json.error === 'Session Expired' || json.error.startsWith('Connection failed')) {
  467. this.logout();
  468. }
  469. throw new Error(json.error);
  470. }
  471. return json.data;
  472. } catch (e) {
  473. throw e;
  474. }
  475. },
  476. async login() {
  477. this.loading = true;
  478. this.errorMsg = '';
  479. try {
  480. // Args: user pass [otp]
  481. const args = [this.loginForm.user, this.loginForm.pass];
  482. if (this.loginForm.otp) args.push(this.loginForm.otp);
  483. const token = await this.api('LOGIN', args);
  484. // If success, token is returned string
  485. // Note: My TCP API returns "OK <token>" string or JSON if modified.
  486. // The proxy handles "OK " stripping. So 'token' variable here holds the raw token string.
  487. this.token = token.trim();
  488. this.username = this.loginForm.user;
  489. localStorage.setItem('raft_token', this.token);
  490. localStorage.setItem('raft_user', this.username);
  491. this.isLoggedIn = true;
  492. this.fetchUsers();
  493. } catch (e) {
  494. this.errorMsg = e.message;
  495. } finally {
  496. this.loading = false;
  497. }
  498. },
  499. logout() {
  500. this.api('LOGOUT');
  501. this.token = '';
  502. this.username = '';
  503. this.isLoggedIn = false;
  504. localStorage.removeItem('raft_token');
  505. },
  506. async fetchUsers() {
  507. try {
  508. this.users = await this.api('USER_LIST');
  509. if (typeof this.users === 'string') {
  510. this.users = JSON.parse(this.users);
  511. }
  512. } catch (e) {
  513. console.error("Failed to fetch users", e);
  514. }
  515. },
  516. async fetchInfo() {
  517. try {
  518. const res = await this.api('INFO');
  519. // TCP returns "OK {json}" or just {json} via proxy parsing?
  520. // api() helper returns json.data.
  521. // If TCP returns "OK <json>", proxy parses it if it looks like JSON.
  522. // Our INFO returns "OK <json>".
  523. this.info = res;
  524. if (typeof this.info === 'string') {
  525. try {
  526. this.info = JSON.parse(this.info);
  527. } catch(e) { /* ignore */ }
  528. }
  529. } catch (e) {
  530. console.error("Failed to fetch info", e);
  531. }
  532. },
  533. async fetchRoles() {
  534. try {
  535. this.roles = await this.api('ROLE_LIST');
  536. } catch (e) {
  537. console.error(e);
  538. if (e.message.includes("unknown command")) {
  539. alert("Server error: ROLE_LIST command not found. Please restart your Raft node to apply latest changes.");
  540. }
  541. }
  542. },
  543. async executeKV() {
  544. this.kvResult = 'Processing...';
  545. this.kvError = false;
  546. try {
  547. const args = [this.kvKey];
  548. if (this.kvOp === 'SET') args.push(this.kvVal);
  549. const res = await this.api(this.kvOp, args);
  550. this.kvResult = typeof res === 'object' ? JSON.stringify(res) : res;
  551. this.kvHistory.push({ op: this.kvOp, key: this.kvKey, time: new Date().toLocaleTimeString() });
  552. } catch (e) {
  553. this.kvError = true;
  554. this.kvResult = e.message;
  555. }
  556. },
  557. async runQuickCmd() {
  558. if(!this.quickCmd) return;
  559. const parts = this.quickCmd.split(' ');
  560. const cmd = parts[0].toUpperCase();
  561. const args = parts.slice(1);
  562. try {
  563. const res = await this.api(cmd, args);
  564. this.quickResult = typeof res === 'object' ? JSON.stringify(res, null, 2) : res;
  565. } catch (e) {
  566. this.quickResult = "Error: " + e.message;
  567. }
  568. },
  569. async createUser() {
  570. try {
  571. await this.api('USER_CREATE', [this.newUser.username, this.newUser.password, this.newUser.roles]);
  572. this.showAddUserModal = false;
  573. this.newUser = { username: '', password: '', roles: '' };
  574. this.fetchUsers();
  575. } catch (e) {
  576. alert(e.message);
  577. }
  578. },
  579. async createRole() {
  580. try {
  581. await this.api('ROLE_CREATE', [this.newRoleName]);
  582. this.showAddRoleModal = false;
  583. this.newRoleName = '';
  584. this.fetchRoles();
  585. } catch (e) {
  586. alert(e.message);
  587. }
  588. },
  589. openAddPermModal(roleName) {
  590. this.selectedRole = roleName;
  591. this.newPerm = { pattern: '', actions: [] };
  592. this.showAddPermModal = true;
  593. },
  594. async addPermission() {
  595. if (!this.newPerm.pattern || this.newPerm.actions.length === 0) {
  596. alert("Please provide pattern and at least one action");
  597. return;
  598. }
  599. try {
  600. // Command: ROLE_PERMISSION_ADD <role> <pattern> <actions>
  601. const actionsStr = this.newPerm.actions.join(',');
  602. await this.api('ROLE_PERMISSION_ADD', [this.selectedRole, this.newPerm.pattern, actionsStr]);
  603. this.showAddPermModal = false;
  604. this.fetchRoles();
  605. } catch (e) {
  606. alert(e.message);
  607. }
  608. },
  609. async unlockUser(username) {
  610. if(!confirm("Unlock user " + username + "?")) return;
  611. try {
  612. await this.api('USER_UNLOCK', [username]);
  613. alert("Unlocked");
  614. } catch (e) { alert(e.message); }
  615. }
  616. }
  617. }
  618. </script>
  619. </body>
  620. </html>