diff --git a/src/frontend/flask_app/blueprints/admin.py b/src/frontend/flask_app/blueprints/admin.py index 2658066..37951a0 100644 --- a/src/frontend/flask_app/blueprints/admin.py +++ b/src/frontend/flask_app/blueprints/admin.py @@ -138,6 +138,21 @@ def api_change_password(user_id: int): return jsonify(resp), 200 +@admin_bp.route("/api/users//pin", methods=["PUT"]) +@login_required +@admin_required +def api_set_pin(user_id: int): + """Proxy: set (4 digits) or remove ({"pin": null}) a user's PIN (#59).""" + data = request.get_json(silent=True) or {} + # Forward only the pin field; backend answers 422 for admins / malformed PINs. + resp = api_client.put(f"/api/users/{user_id}/pin", data={"pin": data.get("pin")}) + + if isinstance(resp, dict) and resp.get("error"): + return jsonify(resp), resp.get("status_code", 500) + + return jsonify(resp), 200 + + @admin_bp.route("/api/users//toggle-active", methods=["POST"]) @login_required @admin_required diff --git a/src/frontend/flask_app/templates/admin/_users_panel.html b/src/frontend/flask_app/templates/admin/_users_panel.html index 68e1ec4..13f0d7c 100644 --- a/src/frontend/flask_app/templates/admin/_users_panel.html +++ b/src/frontend/flask_app/templates/admin/_users_panel.html @@ -55,6 +55,10 @@ + + @@ -97,6 +101,10 @@ + + + +
+ + +
+ + + + diff --git a/src/frontend/flask_app/templates/admin/_users_script.html b/src/frontend/flask_app/templates/admin/_users_script.html index 36449dd..7962976 100644 --- a/src/frontend/flask_app/templates/admin/_users_script.html +++ b/src/frontend/flask_app/templates/admin/_users_script.html @@ -11,6 +11,13 @@ function userManagement(initialUsers) { 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, @@ -21,6 +28,7 @@ function userManagement(initialUsers) { display_name: '', email: '', password: '', + pin: '', // #59: PIN facoltativo alla creazione roles: [], is_admin: false, language_pref: 'it', @@ -52,6 +60,7 @@ function userManagement(initialUsers) { display_name: '', email: '', password: '', + pin: '', // #59 roles: [], is_admin: false, language_pref: 'it', @@ -134,6 +143,12 @@ function userManagement(initialUsers) { 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, @@ -158,6 +173,24 @@ function userManagement(initialUsers) { } 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(); @@ -218,6 +251,70 @@ function userManagement(initialUsers) { } }, + // #59: imposta / reset / togli PIN, usa PUT /admin/api/users//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; diff --git a/src/frontend/flask_app/templates/components/navbar.html b/src/frontend/flask_app/templates/components/navbar.html index b6c26eb..3f8058f 100644 --- a/src/frontend/flask_app/templates/components/navbar.html +++ b/src/frontend/flask_app/templates/components/navbar.html @@ -8,6 +8,8 @@ {% set icon_stations %}{% endset %} {% set icon_settings %}{% endset %} +{% set icon_monitor %}{% endset %} + {# Single nav link, desktop or mobile variant. `icon` is SVG path markup. #} {% macro nav_link(href, label, icon, active=false, mobile=false) -%}