From 4ea512d0072dad1f3a85a7d20dded8466b918ca5 Mon Sep 17 00:00:00 2001 From: bruno Date: Mon, 13 Jul 2026 13:42:11 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20My=20Account=20=E2=80=94=20modifier=20u?= =?UTF-8?q?sername,=20nom,=20email,=20mot=20de=20passe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - PUT /api/settings/account (full_name, login, email, password) - Vérifie que le username n'est pas déjà pris - Password min 6 caractères Frontend (Settings → My Account): - Champs éditables: Username, Full name, Email - Section Change password avec toggle 👁/🙈 - Bouton Save changes + Update password - Message de confirmation ✓ / ✗ avec timeout 3s --- app/routers/dashboard.py | 36 +++++++++++++++++ app/templates/settings.html | 81 ++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index f518452..c29f59a 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -1361,3 +1361,39 @@ async def search_by_tags(request: Request, tags: str = ""): "size_display": _format_size(size), }) return {"items": items} + + +# ── Account update ── +@router.put("/api/settings/account") +async def update_account(request: Request): + """Update current user's profile: full_name, login, email, password.""" + import json + from app.password_utils import hash_password + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return JSONResponse({"error": "Not authenticated"}, status_code=401) + try: + body = await request.json() + except Exception: + body = {} + with get_conn() as conn: + uid = user["id"] + if "full_name" in body: + conn.execute("UPDATE users SET full_name=? WHERE id=?", (body["full_name"].strip(), uid)) + if "login" in body: + new_login = body["login"].strip() + if new_login and new_login != user.get("login"): + existing = conn.execute("SELECT id FROM users WHERE login=? AND id!=?", (new_login, uid)).fetchone() + if existing: + return JSONResponse({"error": "Username already taken"}, status_code=409) + conn.execute("UPDATE users SET login=? WHERE id=?", (new_login, uid)) + if "email" in body: + conn.execute("UPDATE users SET email=? WHERE id=?", (body["email"].strip(), uid)) + if "password" in body and body["password"].strip(): + pw = body["password"].strip() + if len(pw) < 6: + return JSONResponse({"error": "Password must be at least 6 characters"}, status_code=400) + conn.execute("UPDATE users SET password_hash=? WHERE id=?", (hash_password(pw), uid)) + conn.commit() + row = conn.execute("SELECT id, login, full_name, email, is_admin FROM users WHERE id=?", (uid,)).fetchone() + return {"status": "ok", "user": dict(row)} diff --git a/app/templates/settings.html b/app/templates/settings.html index 376115d..588a161 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -150,17 +150,47 @@ -
-
-
Full name
-
+
+ + +
+

Account details

+
+
Username
+ +
+
+
Full name
+ +
+
+
Email
+ +
+
+ + +
+
+ + +
+

Change password

+
+
New password
+
+ +
-
-
-
Email
-
-
+
+
@@ -472,6 +502,11 @@ document.addEventListener('alpine:init', function() { defaultView: localStorage.getItem('fd_default_view') || 'tree', userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'}, userIsAdmin: {{ 'true' if user.get('is_admin') else 'false' }}, + // Account form + accountForm: {login:'{{ user.login or "" }}', full_name:'{{ user.full_name or "" }}', email:'{{ user.email or "" }}', password:''}, + accountSaving: false, + accountMsg: '', + pwVisible: false, // Admin data adminUsers: [], adminStats: {}, @@ -628,6 +663,34 @@ document.addEventListener('alpine:init', function() { } catch(e) {} }, + async saveAccount() { + this.accountSaving = true; + this.accountMsg = ''; + var body = {}; + var f = this.accountForm; + if (f.full_name !== this.userInfo.full_name) body.full_name = f.full_name; + if (f.login !== this.userInfo.login) body.login = f.login; + if (f.email !== this.userInfo.email) body.email = f.email; + if (f.password) body.password = f.password; + if (Object.keys(body).length === 0) { this.accountSaving = false; return; } + try { + var r = await fetch('/api/settings/account', { + method: 'PUT', headers: {'Content-Type':'application/json', 'X-CSRF-Token': this.getCsrfToken()}, + body: JSON.stringify(body) + }); + var d = await r.json(); + if (r.ok) { + this.userInfo = {login: d.user.login, full_name: d.user.full_name, email: d.user.email}; + this.accountForm.password = ''; + this.accountMsg = '✓ Saved!'; + setTimeout(() => { this.accountMsg = ''; }, 3000); + } else { + this.accountMsg = '✗ ' + (d.error || 'Error'); + } + } catch(e) { this.accountMsg = '✗ Network error'; } + this.accountSaving = false; + }, + async uploadAvatar(ev) { var file = ev.target.files[0]; if (!file) return;