|
@@ -6,6 +6,7 @@
|
|
|
<title>Raft KV Admin</title>
|
|
<title>Raft KV Admin</title>
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<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 defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
|
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js"></script>
|
|
|
<script>
|
|
<script>
|
|
|
tailwind.config = {
|
|
tailwind.config = {
|
|
|
darkMode: 'class',
|
|
darkMode: 'class',
|
|
@@ -86,6 +87,12 @@
|
|
|
<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>
|
|
<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
|
|
Roles
|
|
|
</a>
|
|
</a>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="pt-4 pb-2 px-2 text-xs font-bold text-slate-500 uppercase">Account</div>
|
|
|
|
|
+ <a @click="checkMFA(); tab = 'settings'" :class="{'bg-brand-600 text-white shadow-lg shadow-brand-500/20': tab === 'settings', 'hover:bg-dark-700': tab !== 'settings'}" 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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
|
|
|
+ Settings
|
|
|
|
|
+ </a>
|
|
|
</nav>
|
|
</nav>
|
|
|
|
|
|
|
|
<div class="p-4 border-t border-dark-700">
|
|
<div class="p-4 border-t border-dark-700">
|
|
@@ -284,6 +291,52 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Settings Tab -->
|
|
|
|
|
+ <div x-show="tab === 'settings'" class="space-y-6">
|
|
|
|
|
+ <div class="bg-dark-800 rounded-2xl border border-dark-700 p-8">
|
|
|
|
|
+ <h3 class="text-xl font-bold text-white mb-6">Security Settings</h3>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex items-start justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="font-bold text-white text-lg mb-1">Two-Factor Authentication (2FA)</div>
|
|
|
|
|
+ <div class="text-slate-400 max-w-lg">Protect your account by requiring a code from your authenticator app (like Google Authenticator) in addition to your password.</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <template x-if="mfaStatus === 'enabled'">
|
|
|
|
|
+ <button @click="disableMFA" class="bg-red-500/10 text-red-400 border border-red-500/50 px-6 py-2 rounded-lg font-bold hover:bg-red-500/20 transition">Disable 2FA</button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template x-if="mfaStatus !== 'enabled'">
|
|
|
|
|
+ <button @click="startMFASetup" class="bg-brand-600 text-white px-6 py-2 rounded-lg font-bold hover:bg-brand-500 transition">Enable 2FA</button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- MFA Setup Area -->
|
|
|
|
|
+ <div x-show="isSettingUpMFA" class="mt-8 border-t border-dark-700 pt-8" x-transition>
|
|
|
|
|
+ <h4 class="text-lg font-bold text-white mb-4">Setup 2FA</h4>
|
|
|
|
|
+ <div class="flex flex-col md:flex-row gap-8">
|
|
|
|
|
+ <div class="bg-white p-4 rounded-lg inline-block">
|
|
|
|
|
+ <canvas id="qr" width="200" height="200"></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex-1 space-y-4">
|
|
|
|
|
+ <div class="p-4 bg-dark-900 rounded-lg border border-dark-700">
|
|
|
|
|
+ <div class="text-xs text-slate-500 uppercase font-bold mb-1">Secret Key</div>
|
|
|
|
|
+ <div class="font-mono text-xl text-white tracking-wider" x-text="mfaSecret"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label class="block text-sm font-medium mb-2 text-slate-400">Verification Code</label>
|
|
|
|
|
+ <div class="flex gap-2">
|
|
|
|
|
+ <input x-model="mfaCode" type="text" placeholder="000000" maxlength="6" class="w-40 bg-dark-900 border border-dark-700 rounded-lg p-3 text-white text-center font-mono text-lg outline-none focus:border-brand-500">
|
|
|
|
|
+ <button @click="verifyAndEnableMFA" class="bg-green-600 text-white px-6 rounded-lg font-bold hover:bg-green-500">Verify & Enable</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p class="text-sm text-slate-500">Scan the QR code with your authenticator app, or enter the secret key manually. Then enter the 6-digit code to confirm.</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<!-- Users Tab -->
|
|
<!-- Users Tab -->
|
|
|
<div x-show="tab === 'users'" class="space-y-6">
|
|
<div x-show="tab === 'users'" class="space-y-6">
|
|
|
<!-- Actions -->
|
|
<!-- Actions -->
|
|
@@ -321,6 +374,7 @@
|
|
|
</span>
|
|
</span>
|
|
|
</td>
|
|
</td>
|
|
|
<td class="p-4 text-right">
|
|
<td class="p-4 text-right">
|
|
|
|
|
+ <button @click="openEditUserModal(u)" class="text-blue-500 hover:text-blue-400 text-sm mr-3">Edit</button>
|
|
|
<button @click="unlockUser(u.username)" class="text-yellow-500 hover:text-yellow-400 text-sm mr-3">Reset Lock</button>
|
|
<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> -->
|
|
<!-- <button class="text-red-500 hover:text-red-400 text-sm">Delete</button> -->
|
|
|
</td>
|
|
</td>
|
|
@@ -396,6 +450,27 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Edit User Modal -->
|
|
|
|
|
+ <div x-show="showEditUserModal" 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">Edit User <span x-text="editUserForm.username" class="text-brand-500"></span></h3>
|
|
|
|
|
+ <div class="space-y-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label class="block text-sm font-medium mb-1 text-slate-400">New Password (Optional)</label>
|
|
|
|
|
+ <input x-model="editUserForm.password" type="password" placeholder="Leave empty to keep current" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label class="block text-sm font-medium mb-1 text-slate-400">Roles</label>
|
|
|
|
|
+ <input x-model="editUserForm.roles" type="text" placeholder="Roles (comma separated)" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex gap-3 pt-2">
|
|
|
|
|
+ <button @click="updateUser" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Save Changes</button>
|
|
|
|
|
+ <button @click="showEditUserModal = 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 -->
|
|
<!-- 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 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">
|
|
<div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
|
|
@@ -486,6 +561,8 @@
|
|
|
// Modals
|
|
// Modals
|
|
|
showAddUserModal: false,
|
|
showAddUserModal: false,
|
|
|
newUser: { username: '', password: '', roles: '' },
|
|
newUser: { username: '', password: '', roles: '' },
|
|
|
|
|
+ showEditUserModal: false,
|
|
|
|
|
+ editUserForm: { username: '', password: '', roles: '' },
|
|
|
showAddRoleModal: false,
|
|
showAddRoleModal: false,
|
|
|
newRoleName: '',
|
|
newRoleName: '',
|
|
|
|
|
|
|
@@ -495,6 +572,12 @@
|
|
|
selectedRole: '',
|
|
selectedRole: '',
|
|
|
newPerm: { pattern: '', actions: [], min: '', max: '' },
|
|
newPerm: { pattern: '', actions: [], min: '', max: '' },
|
|
|
|
|
|
|
|
|
|
+ // MFA
|
|
|
|
|
+ mfaStatus: 'unknown',
|
|
|
|
|
+ isSettingUpMFA: false,
|
|
|
|
|
+ mfaSecret: '',
|
|
|
|
|
+ mfaCode: '',
|
|
|
|
|
+
|
|
|
init() {
|
|
init() {
|
|
|
if (this.token) {
|
|
if (this.token) {
|
|
|
this.isLoggedIn = true;
|
|
this.isLoggedIn = true;
|
|
@@ -647,6 +730,29 @@
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ openEditUserModal(user) {
|
|
|
|
|
+ this.editUserForm = {
|
|
|
|
|
+ username: user.username,
|
|
|
|
|
+ password: '', // Clear password
|
|
|
|
|
+ roles: (user.roles || []).join(',')
|
|
|
|
|
+ };
|
|
|
|
|
+ this.showEditUserModal = true;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async updateUser() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const u = this.editUserForm.username;
|
|
|
|
|
+ const p = this.editUserForm.password || '-';
|
|
|
|
|
+ const r = this.editUserForm.roles; // allow empty string to clear roles
|
|
|
|
|
+
|
|
|
|
|
+ await this.api('USER_UPDATE', [u, p, r]);
|
|
|
|
|
+ this.showEditUserModal = false;
|
|
|
|
|
+ this.fetchUsers();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ alert(e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
async createRole() {
|
|
async createRole() {
|
|
|
try {
|
|
try {
|
|
|
await this.api('ROLE_CREATE', [this.newRoleName]);
|
|
await this.api('ROLE_CREATE', [this.newRoleName]);
|
|
@@ -719,6 +825,64 @@
|
|
|
await this.api('USER_UNLOCK', [username]);
|
|
await this.api('USER_UNLOCK', [username]);
|
|
|
alert("Unlocked");
|
|
alert("Unlocked");
|
|
|
} catch (e) { alert(e.message); }
|
|
} catch (e) { alert(e.message); }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async checkMFA() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await this.api('MFA-STATUS');
|
|
|
|
|
+ // Expect "OK <status>" -> api() returns "<status>"
|
|
|
|
|
+ this.mfaStatus = res.trim();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("MFA Check failed", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async startMFASetup() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // MFA-GENERATE returns "secret url"
|
|
|
|
|
+ const res = await this.api('MFA-GENERATE');
|
|
|
|
|
+ const parts = res.trim().split(' ');
|
|
|
|
|
+ this.mfaSecret = parts[0];
|
|
|
|
|
+ const url = parts[1];
|
|
|
|
|
+
|
|
|
|
|
+ this.isSettingUpMFA = true;
|
|
|
|
|
+
|
|
|
|
|
+ // Render QR
|
|
|
|
|
+ // Wait for transition
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ new QRious({
|
|
|
|
|
+ element: document.getElementById('qr'),
|
|
|
|
|
+ value: url,
|
|
|
|
|
+ size: 200
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ alert(e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async verifyAndEnableMFA() {
|
|
|
|
|
+ if (!this.mfaCode) return alert("Enter code");
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.api('MFA-ENABLE', [this.mfaSecret, this.mfaCode]);
|
|
|
|
|
+ alert("MFA Enabled Successfully!");
|
|
|
|
|
+ this.isSettingUpMFA = false;
|
|
|
|
|
+ this.mfaCode = '';
|
|
|
|
|
+ this.mfaStatus = 'enabled';
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ alert(e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async disableMFA() {
|
|
|
|
|
+ if (!confirm("Are you sure you want to disable 2FA?")) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.api('MFA-DISABLE');
|
|
|
|
|
+ alert("MFA Disabled");
|
|
|
|
|
+ this.mfaStatus = 'disabled';
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ alert(e.message);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|