3c55550d93
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
356 lines
11 KiB
HTML
356 lines
11 KiB
HTML
<script>
|
|
function userManagement(initialUsers) {
|
|
return {
|
|
users: initialUsers || [],
|
|
search: '',
|
|
showModal: false,
|
|
showToggleConfirm: false,
|
|
// #51: stato del reset password
|
|
showReset: false,
|
|
resetUser: null,
|
|
resetPw: '',
|
|
resetDone: false,
|
|
resetError: '',
|
|
// #59: stato del modal PIN
|
|
showPin: false,
|
|
pinUser: null,
|
|
pinValue: '',
|
|
pinDone: false,
|
|
pinRemoved: false,
|
|
pinError: '',
|
|
isEditing: false,
|
|
editingUserId: null,
|
|
toggleUser: null,
|
|
saving: false,
|
|
errorMsg: '',
|
|
form: {
|
|
username: '',
|
|
display_name: '',
|
|
email: '',
|
|
password: '',
|
|
pin: '', // #59: PIN facoltativo alla creazione
|
|
roles: [],
|
|
is_admin: false,
|
|
language_pref: 'it',
|
|
theme_pref: 'light'
|
|
},
|
|
|
|
get csrfToken() {
|
|
const meta = document.querySelector('meta[name=csrf-token]');
|
|
return meta ? meta.content : '';
|
|
},
|
|
|
|
get filteredUsers() {
|
|
if (!this.search.trim()) return this.users;
|
|
const q = this.search.toLowerCase();
|
|
return this.users.filter(u =>
|
|
u.username.toLowerCase().includes(q) ||
|
|
(u.display_name && u.display_name.toLowerCase().includes(q)) ||
|
|
(u.email && u.email.toLowerCase().includes(q)) ||
|
|
(u.roles && u.roles.some(r => r.toLowerCase().includes(q)))
|
|
);
|
|
},
|
|
|
|
openCreateModal() {
|
|
this.isEditing = false;
|
|
this.editingUserId = null;
|
|
this.errorMsg = '';
|
|
this.form = {
|
|
username: '',
|
|
display_name: '',
|
|
email: '',
|
|
password: '',
|
|
pin: '', // #59
|
|
roles: [],
|
|
is_admin: false,
|
|
language_pref: 'it',
|
|
theme_pref: 'light'
|
|
};
|
|
this.showModal = true;
|
|
},
|
|
|
|
openEditModal(user) {
|
|
this.isEditing = true;
|
|
this.editingUserId = user.id;
|
|
this.errorMsg = '';
|
|
this.form = {
|
|
username: user.username,
|
|
display_name: user.display_name || '',
|
|
email: user.email || '',
|
|
password: '',
|
|
roles: [...(user.roles || [])],
|
|
is_admin: user.is_admin || false,
|
|
language_pref: user.language_pref || 'it',
|
|
theme_pref: user.theme_pref || 'light'
|
|
};
|
|
this.showModal = true;
|
|
},
|
|
|
|
closeModal() {
|
|
this.showModal = false;
|
|
this.errorMsg = '';
|
|
},
|
|
|
|
async saveUser() {
|
|
this.saving = true;
|
|
this.errorMsg = '';
|
|
|
|
try {
|
|
if (this.isEditing) {
|
|
// Update user
|
|
const data = {
|
|
display_name: this.form.display_name,
|
|
email: this.form.email || null,
|
|
roles: this.form.roles,
|
|
is_admin: this.form.is_admin,
|
|
language_pref: this.form.language_pref,
|
|
theme_pref: this.form.theme_pref
|
|
};
|
|
|
|
const resp = await fetch(`/admin/api/users/${this.editingUserId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await resp.json();
|
|
if (!resp.ok) {
|
|
this.errorMsg = result.detail || 'Errore nel salvataggio';
|
|
return;
|
|
}
|
|
|
|
// Update local array
|
|
const idx = this.users.findIndex(u => u.id === this.editingUserId);
|
|
if (idx >= 0) this.users[idx] = result;
|
|
|
|
// Change password if provided
|
|
if (this.form.password) {
|
|
const pwResp = await fetch(`/admin/api/users/${this.editingUserId}/password`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify({ password: this.form.password })
|
|
});
|
|
if (!pwResp.ok) {
|
|
const pwResult = await pwResp.json();
|
|
this.errorMsg = pwResult.detail || 'Errore nel cambio password';
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
// Create user
|
|
if (!this.form.username || !this.form.display_name || !this.form.password) {
|
|
this.errorMsg = '{{ _("Username, nome visualizzato e password sono obbligatori") }}';
|
|
return;
|
|
}
|
|
|
|
// #59: PIN facoltativo, 4 cifre, solo per i non admin
|
|
const pin = this.form.is_admin ? '' : (this.form.pin || '').trim();
|
|
if (pin && !/^\d{4}$/.test(pin)) {
|
|
this.errorMsg = {{ _('Il PIN deve essere di 4 cifre')|tojson }};
|
|
return;
|
|
}
|
|
const data = {
|
|
username: this.form.username,
|
|
password: this.form.password,
|
|
display_name: this.form.display_name,
|
|
email: this.form.email || null,
|
|
roles: this.form.roles,
|
|
is_admin: this.form.is_admin,
|
|
language_pref: this.form.language_pref,
|
|
theme_pref: this.form.theme_pref
|
|
};
|
|
|
|
const resp = await fetch('/admin/api/users', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await resp.json();
|
|
if (!resp.ok) {
|
|
this.errorMsg = result.detail || 'Errore nella creazione';
|
|
return;
|
|
}
|
|
|
|
this.users.push(result);
|
|
|
|
// #59: l'utente esiste gia', il PIN si imposta con la chiamata dedicata
|
|
if (pin) {
|
|
const pinResp = await fetch(`/admin/api/users/${result.id}/pin`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify({ pin: pin })
|
|
});
|
|
if (!pinResp.ok) {
|
|
const pinResult = await pinResp.json().catch(() => ({}));
|
|
// resta aperto in modifica sull'utente appena creato, cosi' non si ricrea
|
|
this.isEditing = true;
|
|
this.editingUserId = result.id;
|
|
this.errorMsg = {{ _('Utente creato, ma PIN non impostato')|tojson }} + ': ' + (pinResult.detail || '');
|
|
return;
|
|
}
|
|
result.has_pin = true;
|
|
}
|
|
}
|
|
|
|
this.closeModal();
|
|
} catch (e) {
|
|
this.errorMsg = '{{ _("Errore di connessione al server") }}';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
// #51: reset password dall'admin, usa PUT /admin/api/users/<id>/password
|
|
openReset(user) {
|
|
this.resetUser = user;
|
|
this.resetPw = '';
|
|
this.resetDone = false;
|
|
this.resetError = '';
|
|
this.showReset = true;
|
|
},
|
|
|
|
closeReset() {
|
|
this.showReset = false;
|
|
this.resetPw = ''; // la password generata non resta in memoria
|
|
this.resetDone = false;
|
|
},
|
|
|
|
generateResetPw() {
|
|
// 12 caratteri senza simboli ambigui (0/O, 1/l/I), da crypto random
|
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
|
const buf = new Uint32Array(12);
|
|
crypto.getRandomValues(buf);
|
|
this.resetPw = Array.from(buf, n => chars[n % chars.length]).join('');
|
|
this.resetError = '';
|
|
},
|
|
|
|
async confirmReset() {
|
|
if (!this.resetPw || this.resetPw.length < 8) {
|
|
this.resetError = {{ _('La password deve avere almeno 8 caratteri')|tojson }};
|
|
return;
|
|
}
|
|
this.saving = true;
|
|
this.resetError = '';
|
|
try {
|
|
const resp = await fetch(`/admin/api/users/${this.resetUser.id}/password`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify({ password: this.resetPw })
|
|
});
|
|
const result = await resp.json();
|
|
if (!resp.ok) {
|
|
this.resetError = result.detail || {{ _('Errore nel cambio password')|tojson }};
|
|
return;
|
|
}
|
|
this.resetDone = true;
|
|
} catch (e) {
|
|
this.resetError = {{ _('Errore di connessione al server')|tojson }};
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
// #59: imposta / reset / togli PIN, usa PUT /admin/api/users/<id>/pin
|
|
openPin(user) {
|
|
this.pinUser = user;
|
|
this.pinValue = '';
|
|
this.pinDone = false;
|
|
this.pinRemoved = false;
|
|
this.pinError = '';
|
|
this.showPin = true;
|
|
},
|
|
|
|
closePin() {
|
|
this.showPin = false;
|
|
this.pinValue = ''; // il PIN generato non resta in memoria
|
|
this.pinDone = false;
|
|
},
|
|
|
|
generatePin() {
|
|
// 4 cifre da crypto random
|
|
const buf = new Uint32Array(4);
|
|
crypto.getRandomValues(buf);
|
|
this.pinValue = Array.from(buf, n => n % 10).join('');
|
|
this.pinError = '';
|
|
},
|
|
|
|
async savePin(pin) {
|
|
// pin = stringa di 4 cifre oppure null (toglie il PIN)
|
|
this.saving = true;
|
|
this.pinError = '';
|
|
try {
|
|
const resp = await fetch(`/admin/api/users/${this.pinUser.id}/pin`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify({ pin: pin })
|
|
});
|
|
const result = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) {
|
|
this.pinError = result.detail || {{ _('Errore nel salvataggio del PIN')|tojson }};
|
|
return;
|
|
}
|
|
// aggiorna l'indicatore locale
|
|
const idx = this.users.findIndex(u => u.id === this.pinUser.id);
|
|
if (idx >= 0) this.users[idx] = Object.assign({}, this.users[idx], { has_pin: pin !== null });
|
|
this.pinUser = idx >= 0 ? this.users[idx] : this.pinUser;
|
|
this.pinRemoved = pin === null;
|
|
this.pinDone = true;
|
|
} catch (e) {
|
|
this.pinError = {{ _('Errore di connessione al server')|tojson }};
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async confirmPin() {
|
|
if (!/^\d{4}$/.test(this.pinValue)) {
|
|
this.pinError = {{ _('Il PIN deve essere di 4 cifre')|tojson }};
|
|
return;
|
|
}
|
|
await this.savePin(this.pinValue);
|
|
},
|
|
|
|
async removePin() {
|
|
await this.savePin(null);
|
|
},
|
|
|
|
toggleActive(user) {
|
|
this.toggleUser = user;
|
|
this.showToggleConfirm = true;
|
|
},
|
|
|
|
async confirmToggle() {
|
|
if (!this.toggleUser) return;
|
|
this.saving = true;
|
|
|
|
try {
|
|
const newActive = !this.toggleUser.active;
|
|
const resp = await fetch(`/admin/api/users/${this.toggleUser.id}/toggle-active`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken },
|
|
body: JSON.stringify({ active: newActive })
|
|
});
|
|
|
|
const result = await resp.json();
|
|
if (!resp.ok) {
|
|
alert(result.detail || 'Errore');
|
|
return;
|
|
}
|
|
|
|
// Update local array
|
|
const idx = this.users.findIndex(u => u.id === this.toggleUser.id);
|
|
if (idx >= 0) this.users[idx] = result;
|
|
|
|
this.showToggleConfirm = false;
|
|
this.toggleUser = null;
|
|
} catch (e) {
|
|
alert('{{ _("Errore di connessione al server") }}');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|