SysOps: approved_backup — 2026-08-16 11:01 UTC
This commit is contained in:
@@ -12,6 +12,9 @@
|
||||
<a href="/settings?tab=email" class="settings-tab {% if active_tab == 'email' %}active{% endif %}">Email</a>
|
||||
<a href="/settings?tab=social" class="settings-tab {% if active_tab == 'social' %}active{% endif %}">Social API's</a>
|
||||
<a href="/settings?tab=permissions" class="settings-tab {% if active_tab == 'permissions' %}active{% endif %}">Rechten Herman</a>
|
||||
{% if can_manage_users %}
|
||||
<a href="/settings?tab=users" class="settings-tab {% if active_tab == 'users' %}active{% endif %}">Gebruikers</a>
|
||||
{% endif %}
|
||||
<a href="/settings?tab=general" class="settings-tab {% if active_tab == 'general' %}active{% endif %}">General</a>
|
||||
</nav>
|
||||
|
||||
@@ -138,6 +141,111 @@
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif active_tab == 'users' and can_manage_users %}
|
||||
<section class="panel settings-panel hm-neo" x-data="usersAdminPage()" x-init="load()">
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
<h2>Gebruikers & rechten</h2>
|
||||
<p class="page-tip">Maak accounts aan en bepaal welke cockpit-pagina's zichtbaar zijn. Admins hebben altijd volledige toegang.</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" @click="openCreate()">+ Nieuwe gebruiker</button>
|
||||
</div>
|
||||
|
||||
<div class="email-account-list">
|
||||
<template x-for="u in users" :key="u.id">
|
||||
<article class="email-account-card" :class="{ 'is-active': u.is_active }">
|
||||
<div class="email-account-card-head">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;min-width:0">
|
||||
<img class="user-card-avatar" x-show="u.avatar_url" :src="u.avatar_url" alt="" />
|
||||
<span class="user-card-avatar fallback" x-show="!u.avatar_url" x-text="initials(u)"></span>
|
||||
<div style="min-width:0">
|
||||
<strong x-text="u.display_name"></strong>
|
||||
<span class="muted" x-text="'@' + u.username"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge" :class="u.role === 'admin' ? 'badge-positive' : 'badge-neutral'" x-text="u.role"></span>
|
||||
</div>
|
||||
<div class="email-account-meta">
|
||||
<span x-text="u.is_active ? 'Actief' : 'Uitgeschakeld'"></span>
|
||||
<span x-text="u.role === 'admin' ? 'Alle pagina\'s' : ((u.permissions.pages||[]).length + ' pagina\'s')"></span>
|
||||
<span x-show="u.last_login_at" x-text="'Laatste login: ' + (u.last_login_at || '')"></span>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="edit(u)">Bewerken</button>
|
||||
<button type="button" class="btn btn-reject btn-sm" @click="remove(u)" x-show="u.id !== meId">Verwijder</button>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<p class="empty-state" x-show="!users.length && !loading">Nog geen gebruikers.</p>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{ open: formOpen }" @click.self="formOpen=false">
|
||||
<div class="modal-card settings-modal" style="max-width:560px">
|
||||
<h3 x-text="form.id ? 'Gebruiker bewerken' : 'Nieuwe gebruiker'"></h3>
|
||||
|
||||
<div class="user-form-avatar-row">
|
||||
<img class="user-form-avatar-preview" x-show="form.avatar_url || form.avatarPreview"
|
||||
:src="form.avatarPreview || form.avatar_url" alt="" />
|
||||
<span class="user-form-avatar-preview fallback" x-show="!form.avatar_url && !form.avatarPreview"
|
||||
x-text="formInitials()"></span>
|
||||
<div>
|
||||
<input type="file" accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
@change="onAvatarPick($event)" />
|
||||
<p class="page-tip" style="margin:0.35rem 0 0">JPG/PNG/WEBP, max 3 MB. Bij nieuwe user: eerst opslaan, daarna foto.</p>
|
||||
<button type="button" class="btn btn-sm btn-secondary" style="margin-top:0.35rem"
|
||||
x-show="form.id && form.avatar_url" @click="removeAvatar()">Foto verwijderen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Gebruikersnaam
|
||||
<input class="form-input" x-model="form.username" :disabled="!!form.id" placeholder="bijv. sandra" autocomplete="off" />
|
||||
</label>
|
||||
<label class="form-label">Weergavenaam
|
||||
<input class="form-input" x-model="form.display_name" placeholder="Sandra" />
|
||||
</label>
|
||||
<label class="form-label">Wachtwoord <span class="muted" x-show="form.id">(leeg = ongewijzigd)</span>
|
||||
<input class="form-input" type="password" x-model="form.password" autocomplete="new-password" />
|
||||
</label>
|
||||
<label class="form-label">Rol
|
||||
<select class="form-input" x-model="form.role">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-check" x-show="form.id">
|
||||
<input type="checkbox" x-model="form.is_active" /> Account actief
|
||||
</label>
|
||||
<label class="form-check">
|
||||
<input type="checkbox" x-model="form.manage_users" :disabled="form.role === 'admin'" /> Mag gebruikers beheren
|
||||
</label>
|
||||
|
||||
<div x-show="form.role !== 'admin'" style="margin-top:0.75rem">
|
||||
<strong style="font-size:0.85rem">Pagina-rechten</strong>
|
||||
<div class="perm-grid" style="margin-top:0.5rem;grid-template-columns:1fr 1fr">
|
||||
<template x-for="p in pages" :key="p.key">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" :value="p.key" :checked="form.pages.includes(p.key)"
|
||||
@change="togglePage(p.key, $event.target.checked)" />
|
||||
<span x-text="p.label"></span>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-top:0.5rem">
|
||||
<button type="button" class="btn btn-sm btn-secondary" @click="selectAllPages()">Alles</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" @click="form.pages=[]">Geen</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="page-tip" x-show="form.role === 'admin'">Admins hebben automatisch toegang tot alle pagina's en gebruikersbeheer.</p>
|
||||
|
||||
<div class="btn-group" style="margin-top:1rem">
|
||||
<button type="button" class="btn btn-primary" @click="save()" :disabled="saving" x-text="saving ? 'Bezig…' : 'Opslaan'"></button>
|
||||
<button type="button" class="btn btn-secondary" @click="formOpen=false">Annuleer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% elif active_tab == 'social' %}
|
||||
<section class="panel settings-panel" x-data="socialSettingsPage()" x-init="load()">
|
||||
<div class="settings-panel-head">
|
||||
@@ -271,6 +379,152 @@ function socialSettingsPage() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function usersAdminPage() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
users: [],
|
||||
pages: [],
|
||||
meId: null,
|
||||
formOpen: false,
|
||||
form: { id: null, username: '', display_name: '', password: '', role: 'user', is_active: true, manage_users: false, pages: [] },
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const me = await Cockpit.api('/api/auth/me');
|
||||
this.meId = me.user && me.user.id;
|
||||
const r = await Cockpit.api('/api/settings/users');
|
||||
this.users = r.users || [];
|
||||
this.pages = r.pages || [];
|
||||
} catch (e) {
|
||||
Cockpit.toast((e && e.message) || 'Laden mislukt', 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
openCreate() {
|
||||
this.form = { id: null, username: '', display_name: '', password: '', role: 'user', is_active: true, manage_users: false, pages: ['dashboard'], avatar_url: null, avatarPreview: null, avatarFile: null };
|
||||
this.formOpen = true;
|
||||
},
|
||||
edit(u) {
|
||||
this.form = {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
display_name: u.display_name || '',
|
||||
password: '',
|
||||
role: u.role || 'user',
|
||||
is_active: !!u.is_active,
|
||||
manage_users: !!(u.permissions && u.permissions.manage_users),
|
||||
pages: (u.permissions && u.permissions.pages) ? [...u.permissions.pages] : [],
|
||||
avatar_url: u.avatar_url || null,
|
||||
avatarPreview: null,
|
||||
avatarFile: null,
|
||||
};
|
||||
this.formOpen = true;
|
||||
},
|
||||
initials(u) {
|
||||
const n = ((u && (u.display_name || u.username)) || '?').trim();
|
||||
const parts = n.split(/\s+/).filter(Boolean);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
return n.slice(0, 2).toUpperCase();
|
||||
},
|
||||
formInitials() {
|
||||
return this.initials({ display_name: this.form.display_name, username: this.form.username });
|
||||
},
|
||||
onAvatarPick(ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
this.form.avatarFile = file;
|
||||
this.form.avatarPreview = URL.createObjectURL(file);
|
||||
},
|
||||
async uploadAvatarFor(userId, file) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const res = await fetch('/api/settings/users/' + userId + '/avatar', { method: 'POST', body: fd, credentials: 'same-origin' });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.detail || 'Foto upload mislukt');
|
||||
return data.user;
|
||||
},
|
||||
async removeAvatar() {
|
||||
if (!this.form.id) return;
|
||||
if (!confirm('Profielfoto verwijderen?')) return;
|
||||
try {
|
||||
await Cockpit.api('/api/settings/users/' + this.form.id + '/avatar', { method: 'DELETE' });
|
||||
this.form.avatar_url = null;
|
||||
this.form.avatarPreview = null;
|
||||
this.form.avatarFile = null;
|
||||
Cockpit.toast('Foto verwijderd', 'success');
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
Cockpit.toast((e && e.message) || 'Mislukt', 'error');
|
||||
}
|
||||
},
|
||||
togglePage(key, on) {
|
||||
if (on && !this.form.pages.includes(key)) this.form.pages.push(key);
|
||||
if (!on) this.form.pages = this.form.pages.filter(k => k !== key);
|
||||
},
|
||||
selectAllPages() { this.form.pages = this.pages.map(p => p.key); },
|
||||
async save() {
|
||||
this.saving = true;
|
||||
try {
|
||||
const permissions = {
|
||||
pages: this.form.role === 'admin' ? this.pages.map(p => p.key) : this.form.pages,
|
||||
manage_users: this.form.role === 'admin' ? true : !!this.form.manage_users,
|
||||
};
|
||||
let saved = null;
|
||||
if (this.form.id) {
|
||||
const body = {
|
||||
display_name: this.form.display_name,
|
||||
role: this.form.role,
|
||||
is_active: this.form.is_active,
|
||||
permissions,
|
||||
};
|
||||
if (this.form.password) body.password = this.form.password;
|
||||
saved = await Cockpit.api('/api/settings/users/' + this.form.id, { method: 'PUT', body: JSON.stringify(body) });
|
||||
saved = saved.user || saved;
|
||||
} else {
|
||||
if (!this.form.username || !this.form.password) {
|
||||
Cockpit.toast('Gebruikersnaam en wachtwoord verplicht', 'error');
|
||||
return;
|
||||
}
|
||||
saved = await Cockpit.api('/api/settings/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: this.form.username,
|
||||
password: this.form.password,
|
||||
display_name: this.form.display_name || this.form.username,
|
||||
role: this.form.role,
|
||||
permissions,
|
||||
}),
|
||||
});
|
||||
saved = saved.user || saved;
|
||||
}
|
||||
if (this.form.avatarFile && saved && saved.id) {
|
||||
await this.uploadAvatarFor(saved.id, this.form.avatarFile);
|
||||
}
|
||||
this.formOpen = false;
|
||||
Cockpit.toast('Gebruiker opgeslagen', 'success');
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
Cockpit.toast((e && e.message) || 'Opslaan mislukt', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async remove(u) {
|
||||
if (!confirm('Verwijder gebruiker ' + u.username + '?')) return;
|
||||
try {
|
||||
await Cockpit.api('/api/settings/users/' + u.id, { method: 'DELETE' });
|
||||
Cockpit.toast('Verwijderd', 'success');
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
Cockpit.toast((e && e.message) || 'Verwijderen mislukt', 'error');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function permissionsPage() {
|
||||
return {
|
||||
perms: [], grantedCount: 0, total: 0,
|
||||
|
||||
Reference in New Issue
Block a user