510 lines
27 KiB
HTML
510 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ t.nav_admin }} | {{ t.site_name }}{% endblock title %}
|
|
|
|
{% block head_extra %}
|
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; min-height: 100vh; background: #f6f7f8; color: #1d252d; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
a { color: inherit; }
|
|
.shell { min-height: 100vh; display: grid; grid-template-columns: 220px minmax(0, 1fr); }
|
|
.sidebar { background: #17202a; color: #f4f7fb; padding: 1rem; }
|
|
.sidebar h1 { font-size: 1.05rem; line-height: 1.25; margin: 0 0 1.25rem; }
|
|
.sidebar a, .sidebar button { width: 100%; display: block; text-align: left; text-decoration: none; color: #d8dee6; padding: .5rem .55rem; border-radius: 4px; margin-bottom: .2rem; border: 0; background: transparent; cursor: pointer; font: inherit; }
|
|
.sidebar a:hover, .sidebar button:hover, .sidebar .active { background: #263544; color: #fff; }
|
|
.main-wrap { min-width: 0; display: flex; flex-direction: column; }
|
|
.topbar { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 1rem; padding: .5rem 1.25rem; border-bottom: 1px solid #dde2e6; background: #fff; }
|
|
.user-info { font-size: .875rem; color: #53606d; }
|
|
.app-version { font-size: .75rem; color: #9aa4ae; white-space: nowrap; }
|
|
.logout-link, .lang-switch a { font-size: .875rem; text-decoration: none; color: #53606d; padding: .25rem .45rem; border-radius: 4px; }
|
|
.logout-link:hover, .lang-switch a:hover { background: #eef1f4; color: #1d252d; }
|
|
.lang-switch a.active { color: #1d252d; font-weight: 700; }
|
|
.main { width: min(1180px, 100%); padding: 1.5rem; }
|
|
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
|
|
.toolbar h2 { font-size: 1.5rem; margin: 0; }
|
|
.actions, .row-actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
|
input, select { min-height: 36px; border: 1px solid #cbd3db; border-radius: 4px; padding: .45rem .55rem; background: #fff; color: #1d252d; width: 100%; }
|
|
input[type="checkbox"] { width: auto; min-height: auto; }
|
|
button { min-height: 36px; border: 1px solid #17202a; border-radius: 4px; padding: .45rem .75rem; background: #17202a; color: #fff; cursor: pointer; }
|
|
button.secondary { background: #fff; color: #17202a; border-color: #cbd3db; }
|
|
button.danger { background: #9d2323; border-color: #9d2323; }
|
|
button:disabled { opacity: .55; cursor: default; }
|
|
table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #dde2e6; border-radius: 6px; overflow: hidden; }
|
|
th, td { text-align: left; padding: .65rem .75rem; border-bottom: 1px solid #edf0f2; font-size: .92rem; vertical-align: middle; }
|
|
th { background: #eef1f4; font-weight: 650; color: #34414f; }
|
|
tr:last-child td { border-bottom: 0; }
|
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .84rem; background: #f1f3f5; padding: .15rem .35rem; border-radius: 3px; overflow-wrap: anywhere; }
|
|
.status { margin: .75rem 0; min-height: 1.25rem; color: #53606d; }
|
|
.error { color: #9d2323; }
|
|
.empty { background: #fff; border: 1px solid #dde2e6; border-radius: 6px; padding: 1rem; color: #53606d; }
|
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .75rem; margin-bottom: 1rem; }
|
|
.summary-tile { background: #fff; border: 1px solid #dde2e6; border-radius: 6px; padding: .8rem; }
|
|
.summary-value { display: block; font-size: 1.6rem; font-weight: 750; line-height: 1.1; }
|
|
.summary-label { color: #53606d; font-size: .85rem; }
|
|
.section-tabs { display: flex; gap: .4rem; margin: .5rem 0 1rem; flex-wrap: wrap; }
|
|
.section-tabs button { min-height: 32px; padding: .35rem .6rem; }
|
|
.section-tabs button.active { background: #17202a; color: #fff; border-color: #17202a; }
|
|
.settings-grid { display: grid; grid-template-columns: minmax(180px, .7fr) minmax(240px, 1fr) auto; gap: .55rem .75rem; align-items: center; background: #fff; border: 1px solid #dde2e6; border-radius: 6px; padding: .75rem; }
|
|
.settings-row { display: contents; }
|
|
.field-name { font-weight: 650; color: #34414f; overflow-wrap: anywhere; }
|
|
.field-help { color: #53606d; font-size: .78rem; margin-top: .15rem; overflow-wrap: anywhere; }
|
|
.badge { display: inline-block; padding: .15rem .55rem; border-radius: 4px; font-size: .8rem; font-weight: 650; }
|
|
.badge-default { background: #e8ecef; color: #53606d; }
|
|
.badge-database { background: #d7edf1; color: #0f5560; }
|
|
.badge-env { background: #fff0c2; color: #6f4e00; }
|
|
.servers-list { display: grid; gap: .75rem; }
|
|
.server-admin-row { background: #fff; border: 1px solid #dde2e6; border-radius: 6px; padding: .8rem; display: grid; grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.2fr) auto; gap: .75rem; align-items: center; }
|
|
.server-admin-name { display: grid; gap: .25rem; min-width: 0; }
|
|
.server-admin-name label { font-weight: 650; color: #34414f; font-size: .85rem; }
|
|
.server-admin-meta { display: grid; gap: .25rem; min-width: 0; }
|
|
.server-admin-meta span { color: #53606d; font-size: .78rem; }
|
|
.toggle-row { display: inline-flex; align-items: center; gap: .45rem; color: #34414f; font-weight: 650; white-space: nowrap; }
|
|
.modal-backdrop { position: fixed; inset: 0; background: rgba(23,32,42,.35); display: grid; place-items: center; padding: 1rem; z-index: 20; }
|
|
.modal { width: min(560px, 100%); background: #fff; border-radius: 6px; border: 1px solid #dde2e6; box-shadow: 0 16px 48px rgba(23,32,42,.24); padding: 1rem; }
|
|
.modal h3 { margin: 0 0 .75rem; font-size: 1.1rem; }
|
|
.form-grid { display: grid; gap: .7rem; }
|
|
.form-row label { display: block; font-weight: 650; color: #34414f; font-size: .9rem; margin-bottom: .25rem; }
|
|
.hint { color: #53606d; font-size: .78rem; margin-top: .2rem; }
|
|
.debug-grid { display: grid; gap: 1rem; }
|
|
.debug-build { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .55rem; }
|
|
@media (max-width: 760px) {
|
|
.shell { grid-template-columns: 1fr; }
|
|
.sidebar { display: flex; align-items: center; gap: .75rem; overflow-x: auto; }
|
|
.sidebar h1 { margin: 0; white-space: nowrap; }
|
|
.sidebar a, .sidebar button { margin: 0; white-space: nowrap; width: auto; }
|
|
.toolbar { align-items: stretch; flex-direction: column; }
|
|
.actions { align-items: stretch; }
|
|
.actions button { width: 100%; }
|
|
.main { padding: 1rem; }
|
|
table { display: block; overflow-x: auto; }
|
|
.settings-grid { grid-template-columns: 1fr; }
|
|
.server-admin-row { grid-template-columns: 1fr; align-items: stretch; }
|
|
.toggle-row { justify-content: space-between; }
|
|
}
|
|
</style>
|
|
{% endblock head_extra %}
|
|
|
|
{% block body %}
|
|
<div class="shell" x-data="adminApp('{{ initial_view }}')" x-init="init()">
|
|
<nav class="sidebar">
|
|
<h1>{{ t.site_name }}</h1>
|
|
<a href="/configs">{{ t.nav_configs }}</a>
|
|
<button :class="{ active: view === 'dashboard' }" @click="navigate('dashboard')">{{ t.nav_dashboard }}</button>
|
|
<button :class="{ active: view === 'users' }" @click="navigate('users')">{{ t.nav_users }}</button>
|
|
<button :class="{ active: view === 'servers' }" @click="navigate('servers')">{{ t.nav_servers }}</button>
|
|
<button :class="{ active: view === 'settings' }" @click="navigate('settings')">{{ t.nav_settings }}</button>
|
|
<button :class="{ active: view === 'debug' }" @click="navigate('debug')">{{ t.nav_debug }}</button>
|
|
</nav>
|
|
|
|
<div class="main-wrap">
|
|
<div class="topbar">
|
|
<span class="app-version">v{{ app_version }}</span>
|
|
<span class="user-info">{{ user_name }} ({{ user_role }})</span>
|
|
<div class="lang-switch">
|
|
<a href="#"{% if t.lang.code() == "en" %} class="active"{% endif %} onclick="location.href='/set-lang?lang=en&next='+encodeURIComponent(location.pathname);return false">EN</a>
|
|
<a href="#"{% if t.lang.code() == "ru" %} class="active"{% endif %} onclick="location.href='/set-lang?lang=ru&next='+encodeURIComponent(location.pathname);return false">RU</a>
|
|
</div>
|
|
<a class="logout-link" href="/logout">{{ t.nav_logout }}</a>
|
|
</div>
|
|
|
|
<main class="main">
|
|
<div class="toolbar">
|
|
<h2 x-text="title()"></h2>
|
|
<div class="actions">
|
|
<button class="secondary" @click="reload()" :disabled="busy">{{ t.configs_refresh }}</button>
|
|
<template x-if="view === 'users'">
|
|
<button @click="openUserModal(null)" :disabled="busy">{{ t.users_add }}</button>
|
|
</template>
|
|
<template x-if="view === 'settings'">
|
|
<button @click="saveSettings()" :disabled="busy">{{ t.settings_save }}</button>
|
|
</template>
|
|
<template x-if="view === 'servers'">
|
|
<button @click="saveServers()" :disabled="busy">{{ t.settings_save }}</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status" :class="{ error: error }" x-text="error || status"></div>
|
|
|
|
<section x-show="view === 'dashboard'">
|
|
<template x-if="summary">
|
|
<div>
|
|
<div class="summary-grid">
|
|
<div class="summary-tile"><span class="summary-value" x-text="summary.users_count"></span><span class="summary-label">{{ t.nav_users }}</span></div>
|
|
<div class="summary-tile"><span class="summary-value" x-text="summary.admin_users_count"></span><span class="summary-label">admin</span></div>
|
|
<div class="summary-tile"><span class="summary-value" x-text="summary.client_users_count"></span><span class="summary-label">client</span></div>
|
|
<div class="summary-tile"><span class="summary-value" x-text="summary.active_users_count"></span><span class="summary-label">{{ t.users_active }}</span></div>
|
|
</div>
|
|
<div class="debug-build">
|
|
<div class="summary-tile"><span class="summary-label">Version</span><br><code x-text="summary.build.pkg_version"></code></div>
|
|
<div class="summary-tile"><span class="summary-label">Profile</span><br><code x-text="summary.build.profile"></code></div>
|
|
<div class="summary-tile"><span class="summary-label">Target</span><br><code x-text="summary.build.target"></code></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
|
|
<section x-show="view === 'users'">
|
|
<template x-if="users.length === 0 && !busy">
|
|
<div class="empty">{{ t.users_heading }}: 0</div>
|
|
</template>
|
|
<template x-if="users.length > 0">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t.users_username }}</th>
|
|
<th>{{ t.users_email }}</th>
|
|
<th>{{ t.users_display_name }}</th>
|
|
<th>{{ t.users_role }}</th>
|
|
<th>{{ t.users_active }}</th>
|
|
<th>{{ t.users_actions }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="user in users" :key="user.id">
|
|
<tr>
|
|
<td x-text="user.username"></td>
|
|
<td x-text="user.email"></td>
|
|
<td x-text="user.display_name"></td>
|
|
<td><code x-text="user.role"></code></td>
|
|
<td x-text="user.active ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'"></td>
|
|
<td>
|
|
<div class="row-actions">
|
|
<button class="secondary" @click="openUserModal(user)">{{ t.users_edit }}</button>
|
|
<button class="danger" @click="deleteUser(user)">{{ t.users_delete }}</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
</section>
|
|
|
|
<section x-show="view === 'settings'">
|
|
<div class="section-tabs">
|
|
<template x-for="section in settingSections" :key="section">
|
|
<button class="secondary" :class="{ active: settingsSection === section }" @click="settingsSection = section" x-text="sectionLabel(section)"></button>
|
|
</template>
|
|
</div>
|
|
<template x-if="settings.fields.length > 0">
|
|
<div class="settings-grid">
|
|
<template x-for="field in visibleSettings()" :key="field.key">
|
|
<div class="settings-row">
|
|
<div class="field-name">
|
|
<span x-text="fieldLabel(field.key)"></span>
|
|
<div class="field-help" x-text="field.env_var"></div>
|
|
</div>
|
|
<div>
|
|
<template x-if="field.kind === 'bool'">
|
|
<input type="checkbox" x-model="settingsForm[field.key]">
|
|
</template>
|
|
<template x-if="field.kind !== 'bool'">
|
|
<input :type="field.kind === 'password' ? 'password' : field.kind" x-model="settingsForm[field.key]">
|
|
</template>
|
|
<div class="hint">default: <code x-text="field.default_value || '(empty)'"></code></div>
|
|
</div>
|
|
<span class="badge" :class="`badge-${field.source}`" x-text="field.source"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
|
|
<section x-show="view === 'servers'">
|
|
<template x-if="servers.length === 0 && !busy">
|
|
<div class="empty">{{ t.servers_empty }}</div>
|
|
</template>
|
|
<template x-if="servers.length > 0">
|
|
<div class="servers-list">
|
|
<template x-for="server in servers" :key="server.name">
|
|
<div class="server-admin-row">
|
|
<div class="server-admin-name">
|
|
<label>{{ t.servers_display_name }}</label>
|
|
<input x-model="server.display_name" :placeholder="server.name">
|
|
</div>
|
|
<div class="server-admin-meta">
|
|
<span>{{ t.servers_technical_name }}</span>
|
|
<code x-text="server.name"></code>
|
|
<span>{{ t.servers_endpoint }}</span>
|
|
<code x-text="server.endpoint"></code>
|
|
</div>
|
|
<label class="toggle-row">
|
|
<span>{{ t.configs_enabled }}</span>
|
|
<input type="checkbox" x-model="server.enabled">
|
|
</label>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
|
|
<section x-show="view === 'debug'" class="debug-grid">
|
|
<template x-if="debug">
|
|
<div>
|
|
<div class="debug-build">
|
|
<div class="summary-tile"><span class="summary-label">Package</span><br><code x-text="debug.build.pkg_name"></code></div>
|
|
<div class="summary-tile"><span class="summary-label">Version</span><br><code x-text="debug.build.pkg_version"></code></div>
|
|
<div class="summary-tile"><span class="summary-label">Rustc</span><br><code x-text="debug.build.rustc_version"></code></div>
|
|
<div class="summary-tile"><span class="summary-label">{{ t.debug_db_status }}</span><br><code x-text="debug.db_status"></code></div>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t.debug_field }}</th>
|
|
<th>{{ t.debug_value }}</th>
|
|
<th>{{ t.debug_source }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="entry in debug.config_entries" :key="entry.key">
|
|
<tr>
|
|
<td><code x-text="entry.key"></code><div class="hint" x-text="entry.env_var"></div></td>
|
|
<td><code x-text="entry.value || '(empty)'"></code></td>
|
|
<td><span class="badge" :class="`badge-${entry.source}`" x-text="entry.source"></span></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<template x-if="userModalOpen">
|
|
<div class="modal-backdrop" @click.self="closeUserModal()">
|
|
<div class="modal">
|
|
<h3 x-text="userForm.id ? '{{ t.users_edit_heading }}' : '{{ t.users_new_heading }}'"></h3>
|
|
<div class="form-grid">
|
|
<div class="form-row"><label>{{ t.users_username }}</label><input x-model="userForm.username"></div>
|
|
<div class="form-row"><label>{{ t.users_email }}</label><input type="email" x-model="userForm.email"></div>
|
|
<div class="form-row"><label>{{ t.users_display_name }}</label><input x-model="userForm.display_name"></div>
|
|
<div class="form-row">
|
|
<label>{{ t.login_password }}</label>
|
|
<input type="password" x-model="userForm.password">
|
|
<template x-if="userForm.id"><div class="hint">{{ t.users_password_hint }}</div></template>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>{{ t.users_role }}</label>
|
|
<select x-model="userForm.role">
|
|
<option value="client">client</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
</div>
|
|
<div class="actions">
|
|
<button @click="saveUser()">{{ t.settings_save }}</button>
|
|
<button class="secondary" @click="closeUserModal()">{{ t.admin_close }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<script>
|
|
function adminApp(initialView) {
|
|
return {
|
|
view: initialView || 'dashboard',
|
|
busy: false,
|
|
status: '',
|
|
error: '',
|
|
summary: null,
|
|
users: [],
|
|
servers: [],
|
|
settings: { fields: [] },
|
|
settingsForm: {},
|
|
settingsSection: 'auth',
|
|
settingSections: ['auth', 'oidc', 'kubernetes', 'vpn', 'api'],
|
|
debug: null,
|
|
userModalOpen: false,
|
|
userForm: {},
|
|
init() {
|
|
this.loadForView();
|
|
window.addEventListener('popstate', () => {
|
|
this.view = this.viewFromPath(location.pathname);
|
|
this.loadForView();
|
|
});
|
|
},
|
|
async request(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
headers: { 'content-type': 'application/json' },
|
|
...options,
|
|
});
|
|
const data = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(data.error || response.statusText);
|
|
return data;
|
|
},
|
|
navigate(view) {
|
|
this.view = view;
|
|
history.pushState({}, '', this.pathForView(view));
|
|
this.loadForView();
|
|
},
|
|
pathForView(view) {
|
|
return {
|
|
dashboard: '/admin/',
|
|
users: '/admin/users',
|
|
servers: '/admin/servers',
|
|
settings: '/admin/settings',
|
|
debug: '/admin/debug',
|
|
}[view] || '/admin/';
|
|
},
|
|
viewFromPath(path) {
|
|
if (path.endsWith('/users')) return 'users';
|
|
if (path.endsWith('/servers')) return 'servers';
|
|
if (path.endsWith('/settings')) return 'settings';
|
|
if (path.endsWith('/debug')) return 'debug';
|
|
return 'dashboard';
|
|
},
|
|
title() {
|
|
return {
|
|
dashboard: '{{ t.nav_dashboard }}',
|
|
users: '{{ t.nav_users }}',
|
|
servers: '{{ t.nav_servers }}',
|
|
settings: '{{ t.nav_settings }}',
|
|
debug: '{{ t.nav_debug }}',
|
|
}[this.view] || '{{ t.nav_admin }}';
|
|
},
|
|
async loadForView() {
|
|
this.error = '';
|
|
if (this.view === 'dashboard') await this.loadSummary();
|
|
if (this.view === 'users') await this.loadUsers();
|
|
if (this.view === 'servers') await this.loadServers();
|
|
if (this.view === 'settings') await this.loadSettings();
|
|
if (this.view === 'debug') await this.loadDebug();
|
|
},
|
|
async reload() {
|
|
await this.loadForView();
|
|
},
|
|
async loadSummary() {
|
|
this.busy = true;
|
|
try { this.summary = await this.request('/admin/api/summary'); }
|
|
catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async loadUsers() {
|
|
this.busy = true;
|
|
try { this.users = (await this.request('/admin/api/users')).users; }
|
|
catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async loadServers() {
|
|
this.busy = true;
|
|
try { this.servers = (await this.request('/admin/api/servers')).servers; }
|
|
catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async loadSettings() {
|
|
this.busy = true;
|
|
try {
|
|
this.settings = await this.request('/admin/api/settings');
|
|
const form = {};
|
|
for (const field of this.settings.fields) {
|
|
form[field.key] = field.kind === 'bool' ? field.value === 'true' : field.value;
|
|
}
|
|
this.settingsForm = form;
|
|
} catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async loadDebug() {
|
|
this.busy = true;
|
|
try { this.debug = await this.request('/admin/api/debug'); }
|
|
catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
visibleSettings() {
|
|
return this.settings.fields.filter((field) => field.section === this.settingsSection);
|
|
},
|
|
async saveSettings() {
|
|
this.error = '';
|
|
this.status = '';
|
|
this.busy = true;
|
|
try {
|
|
const body = { ...this.settingsForm, vpn_mtu: Number(this.settingsForm.vpn_mtu || 0) };
|
|
this.settings = await this.request('/admin/api/settings', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
this.status = '{{ t.settings_saved }}';
|
|
} catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
async saveServers() {
|
|
this.error = '';
|
|
this.status = '';
|
|
this.busy = true;
|
|
try {
|
|
const data = await this.request('/admin/api/servers', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ servers: this.servers }),
|
|
});
|
|
this.servers = data.servers;
|
|
this.status = '{{ t.settings_saved }}';
|
|
} catch (e) { this.error = e.message; }
|
|
finally { this.busy = false; }
|
|
},
|
|
openUserModal(user) {
|
|
this.userForm = user ? { ...user, password: '' } : {
|
|
id: null, username: '', email: '', display_name: '', password: '', role: 'client',
|
|
};
|
|
this.userModalOpen = true;
|
|
},
|
|
closeUserModal() {
|
|
this.userModalOpen = false;
|
|
this.userForm = {};
|
|
},
|
|
async saveUser() {
|
|
this.error = '';
|
|
this.status = '';
|
|
const isEdit = Boolean(this.userForm.id);
|
|
const url = isEdit ? `/admin/api/users/${this.userForm.id}` : '/admin/api/users';
|
|
try {
|
|
const data = await this.request(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(this.userForm),
|
|
});
|
|
if (isEdit) {
|
|
const index = this.users.findIndex((user) => user.id === data.user.id);
|
|
if (index !== -1) this.users[index] = data.user;
|
|
} else {
|
|
this.users.push(data.user);
|
|
}
|
|
this.closeUserModal();
|
|
this.status = '{{ t.settings_saved }}';
|
|
} catch (e) { this.error = e.message; }
|
|
},
|
|
async deleteUser(user) {
|
|
if (!confirm('{{ t.users_delete_confirm }}')) return;
|
|
this.error = '';
|
|
this.status = '';
|
|
try {
|
|
await this.request(`/admin/api/users/${user.id}/delete`, { method: 'POST' });
|
|
this.users = this.users.filter((item) => item.id !== user.id);
|
|
this.status = '{{ t.settings_saved }}';
|
|
} catch (e) { this.error = e.message; }
|
|
},
|
|
sectionLabel(section) {
|
|
return {
|
|
auth: '{{ t.settings_auth }}',
|
|
oidc: '{{ t.settings_oidc }}',
|
|
kubernetes: '{{ t.settings_kubernetes }}',
|
|
vpn: '{{ t.settings_vpn }}',
|
|
api: '{{ t.settings_api }}',
|
|
}[section] || section;
|
|
},
|
|
fieldLabel(key) {
|
|
return {
|
|
auth_password_enabled: '{{ t.settings_password_login }}',
|
|
auth_sso_enabled: '{{ t.settings_sso_login }}',
|
|
oidc_button_text: '{{ t.settings_oidc_button }}',
|
|
oidc_admin_groups: '{{ t.settings_oidc_admin_groups }}',
|
|
oidc_client_groups: '{{ t.settings_oidc_client_groups }}',
|
|
swagger_enabled: '{{ t.settings_swagger }}',
|
|
}[key] || key;
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock body %}
|