index.html 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  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 max-h-60 overflow-y-auto pr-1 custom-scrollbar">
  330. <template x-for="p in (r.permissions || [])">
  331. <div class="text-sm text-slate-400 font-mono bg-dark-900/50 p-2 rounded flex justify-between items-center group/perm hover:bg-dark-900 transition">
  332. <div>
  333. <span x-text="p.key" class="font-bold text-slate-300"></span>
  334. <span class="text-brand-500 text-xs ml-2" x-text="'[' + p.actions.join(',') + ']'"></span>
  335. </div>
  336. <div class="flex gap-2 opacity-0 group-hover/perm:opacity-100 transition-opacity">
  337. <button @click="openAddPermModal(r.name, p)" class="text-blue-400 hover:text-blue-300" title="Edit">
  338. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
  339. </button>
  340. <button @click="deletePermission(r.name, p.key)" class="text-red-400 hover:text-red-300" title="Delete">
  341. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
  342. </button>
  343. </div>
  344. </div>
  345. </template>
  346. <div x-show="(r.permissions || []).length === 0" class="text-xs text-slate-500 italic p-2 text-center">No rules defined</div>
  347. </div>
  348. <button @click="openAddPermModal(r.name)" class="w-full py-2 bg-dark-700 hover:bg-dark-600 text-sm text-white rounded-lg transition">
  349. Add Permission
  350. </button>
  351. </div>
  352. </template>
  353. </div>
  354. </div>
  355. </div>
  356. </main>
  357. </div>
  358. <!-- Modals -->
  359. <!-- Add User Modal -->
  360. <div x-show="showAddUserModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  361. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  362. <h3 class="text-xl font-bold text-white mb-4">Create New User</h3>
  363. <div class="space-y-4">
  364. <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">
  365. <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">
  366. <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">
  367. <div class="flex gap-3 pt-2">
  368. <button @click="createUser" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  369. <button @click="showAddUserModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  370. </div>
  371. </div>
  372. </div>
  373. </div>
  374. <!-- Add Role Modal -->
  375. <div x-show="showAddRoleModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  376. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  377. <h3 class="text-xl font-bold text-white mb-4">Create New Role</h3>
  378. <div class="space-y-4">
  379. <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">
  380. <div class="flex gap-3 pt-2">
  381. <button @click="createRole" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Create</button>
  382. <button @click="showAddRoleModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  383. </div>
  384. </div>
  385. </div>
  386. </div>
  387. <!-- Add Permission Modal -->
  388. <div x-show="showAddPermModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
  389. <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
  390. <h3 class="text-xl font-bold text-white mb-4"><span x-text="isEditing ? 'Edit' : 'Add'"></span> Permission for <span x-text="selectedRole" class="text-brand-500"></span></h3>
  391. <div class="space-y-4">
  392. <div>
  393. <label class="block text-sm font-medium mb-2 text-slate-400">Key Pattern</label>
  394. <input x-model="newPerm.pattern" :disabled="isEditing" :class="{'opacity-50 cursor-not-allowed': isEditing}" 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">
  395. </div>
  396. <div>
  397. <label class="block text-sm font-medium mb-2 text-slate-400">Actions</label>
  398. <div class="flex gap-4 p-3 bg-dark-900 rounded-lg border border-dark-700">
  399. <label class="flex items-center gap-2 text-white cursor-pointer">
  400. <input type="checkbox" value="read" x-model="newPerm.actions" class="accent-brand-500"> Read
  401. </label>
  402. <label class="flex items-center gap-2 text-white cursor-pointer">
  403. <input type="checkbox" value="write" x-model="newPerm.actions" class="accent-brand-500"> Write
  404. </label>
  405. <label class="flex items-center gap-2 text-white cursor-pointer">
  406. <input type="checkbox" value="admin" x-model="newPerm.actions" class="accent-brand-500"> Admin
  407. </label>
  408. </div>
  409. </div>
  410. <div class="flex gap-3 pt-2">
  411. <button @click="addPermission" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500" x-text="isEditing ? 'Save' : 'Add'"></button>
  412. <button @click="showAddPermModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
  413. </div>
  414. </div>
  415. </div>
  416. </div>
  417. <script>
  418. function app() {
  419. return {
  420. isLoggedIn: false,
  421. token: localStorage.getItem('raft_token') || '',
  422. username: localStorage.getItem('raft_user') || '',
  423. tab: 'dashboard',
  424. loading: false,
  425. errorMsg: '',
  426. // Login Form
  427. loginForm: { user: '', pass: '', otp: '' },
  428. // Data
  429. users: [],
  430. roles: [],
  431. kvHistory: [], // Track KV ops
  432. info: null, // System Info
  433. // KV Op
  434. kvOp: 'GET',
  435. kvKey: '',
  436. kvVal: '',
  437. kvResult: '',
  438. kvError: false,
  439. // Quick Command
  440. quickCmd: '',
  441. quickResult: '',
  442. // Modals
  443. showAddUserModal: false,
  444. newUser: { username: '', password: '', roles: '' },
  445. showAddRoleModal: false,
  446. newRoleName: '',
  447. // Add Permission Modal
  448. showAddPermModal: false,
  449. isEditing: false,
  450. selectedRole: '',
  451. newPerm: { pattern: '', actions: [] },
  452. init() {
  453. if (this.token) {
  454. this.isLoggedIn = true;
  455. this.fetchUsers();
  456. this.fetchInfo();
  457. // Refresh info periodically
  458. setInterval(() => {
  459. if (this.isLoggedIn && this.tab === 'dashboard') {
  460. this.fetchInfo();
  461. }
  462. }, 2000);
  463. }
  464. },
  465. async api(command, args = []) {
  466. try {
  467. const res = await fetch('/api/proxy', {
  468. method: 'POST',
  469. headers: {
  470. 'Content-Type': 'application/json',
  471. 'X-Session-Token': this.token
  472. },
  473. body: JSON.stringify({ command, args })
  474. });
  475. const json = await res.json();
  476. if (json.status === 'error') {
  477. if (json.error === 'Session Expired' || json.error.startsWith('Connection failed')) {
  478. this.logout();
  479. }
  480. throw new Error(json.error);
  481. }
  482. return json.data;
  483. } catch (e) {
  484. throw e;
  485. }
  486. },
  487. async login() {
  488. this.loading = true;
  489. this.errorMsg = '';
  490. try {
  491. // Args: user pass [otp]
  492. const args = [this.loginForm.user, this.loginForm.pass];
  493. if (this.loginForm.otp) args.push(this.loginForm.otp);
  494. const token = await this.api('LOGIN', args);
  495. // If success, token is returned string
  496. // Note: My TCP API returns "OK <token>" string or JSON if modified.
  497. // The proxy handles "OK " stripping. So 'token' variable here holds the raw token string.
  498. this.token = token.trim();
  499. this.username = this.loginForm.user;
  500. localStorage.setItem('raft_token', this.token);
  501. localStorage.setItem('raft_user', this.username);
  502. this.isLoggedIn = true;
  503. this.fetchUsers();
  504. } catch (e) {
  505. this.errorMsg = e.message;
  506. } finally {
  507. this.loading = false;
  508. }
  509. },
  510. logout() {
  511. this.api('LOGOUT');
  512. this.token = '';
  513. this.username = '';
  514. this.isLoggedIn = false;
  515. localStorage.removeItem('raft_token');
  516. },
  517. async fetchUsers() {
  518. try {
  519. this.users = await this.api('USER_LIST');
  520. if (typeof this.users === 'string') {
  521. this.users = JSON.parse(this.users);
  522. }
  523. } catch (e) {
  524. console.error("Failed to fetch users", e);
  525. }
  526. },
  527. async fetchInfo() {
  528. try {
  529. const res = await this.api('INFO');
  530. // TCP returns "OK {json}" or just {json} via proxy parsing?
  531. // api() helper returns json.data.
  532. // If TCP returns "OK <json>", proxy parses it if it looks like JSON.
  533. // Our INFO returns "OK <json>".
  534. this.info = res;
  535. if (typeof this.info === 'string') {
  536. try {
  537. this.info = JSON.parse(this.info);
  538. } catch(e) { /* ignore */ }
  539. }
  540. } catch (e) {
  541. console.error("Failed to fetch info", e);
  542. }
  543. },
  544. async fetchRoles() {
  545. try {
  546. this.roles = await this.api('ROLE_LIST');
  547. } catch (e) {
  548. console.error(e);
  549. if (e.message.includes("unknown command")) {
  550. alert("Server error: ROLE_LIST command not found. Please restart your Raft node to apply latest changes.");
  551. }
  552. }
  553. },
  554. async executeKV() {
  555. this.kvResult = 'Processing...';
  556. this.kvError = false;
  557. try {
  558. const args = [this.kvKey];
  559. if (this.kvOp === 'SET') args.push(this.kvVal);
  560. const res = await this.api(this.kvOp, args);
  561. this.kvResult = typeof res === 'object' ? JSON.stringify(res) : res;
  562. this.kvHistory.push({ op: this.kvOp, key: this.kvKey, time: new Date().toLocaleTimeString() });
  563. } catch (e) {
  564. this.kvError = true;
  565. this.kvResult = e.message;
  566. }
  567. },
  568. async runQuickCmd() {
  569. if(!this.quickCmd) return;
  570. const parts = this.quickCmd.split(' ');
  571. const cmd = parts[0].toUpperCase();
  572. const args = parts.slice(1);
  573. try {
  574. const res = await this.api(cmd, args);
  575. this.quickResult = typeof res === 'object' ? JSON.stringify(res, null, 2) : res;
  576. } catch (e) {
  577. this.quickResult = "Error: " + e.message;
  578. }
  579. },
  580. async createUser() {
  581. try {
  582. await this.api('USER_CREATE', [this.newUser.username, this.newUser.password, this.newUser.roles]);
  583. this.showAddUserModal = false;
  584. this.newUser = { username: '', password: '', roles: '' };
  585. this.fetchUsers();
  586. } catch (e) {
  587. alert(e.message);
  588. }
  589. },
  590. async createRole() {
  591. try {
  592. await this.api('ROLE_CREATE', [this.newRoleName]);
  593. this.showAddRoleModal = false;
  594. this.newRoleName = '';
  595. this.fetchRoles();
  596. } catch (e) {
  597. alert(e.message);
  598. }
  599. },
  600. openAddPermModal(roleName, perm = null) {
  601. this.selectedRole = roleName;
  602. if (perm) {
  603. this.isEditing = true;
  604. this.newPerm = { pattern: perm.key, actions: [...perm.actions] };
  605. } else {
  606. this.isEditing = false;
  607. this.newPerm = { pattern: '', actions: [] };
  608. }
  609. this.showAddPermModal = true;
  610. },
  611. async deletePermission(roleName, pattern) {
  612. if (!confirm(`Remove permission for ${pattern} from ${roleName}?`)) return;
  613. try {
  614. await this.api('ROLE_PERMISSION_REMOVE', [roleName, pattern]);
  615. this.fetchRoles();
  616. } catch (e) {
  617. alert(e.message);
  618. }
  619. },
  620. async addPermission() {
  621. if (!this.newPerm.pattern || this.newPerm.actions.length === 0) {
  622. alert("Please provide pattern and at least one action");
  623. return;
  624. }
  625. try {
  626. // Command: ROLE_PERMISSION_ADD <role> <pattern> <actions>
  627. const actionsStr = this.newPerm.actions.join(',');
  628. await this.api('ROLE_PERMISSION_ADD', [this.selectedRole, this.newPerm.pattern, actionsStr]);
  629. this.showAddPermModal = false;
  630. this.fetchRoles();
  631. } catch (e) {
  632. alert(e.message);
  633. }
  634. },
  635. async unlockUser(username) {
  636. if(!confirm("Unlock user " + username + "?")) return;
  637. try {
  638. await this.api('USER_UNLOCK', [username]);
  639. alert("Unlocked");
  640. } catch (e) { alert(e.message); }
  641. }
  642. }
  643. }
  644. </script>
  645. </body>
  646. </html>