feat: My Account — modifier username, nom, email, mot de passe
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
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -150,17 +150,47 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="setting-label">Full name</div>
|
||||
<div class="setting-desc" x-text="userInfo.full_name || userInfo.login"></div>
|
||||
</div>
|
||||
|
||||
<!-- Editable fields -->
|
||||
<div class="setting-group">
|
||||
<h3>Account details</h3>
|
||||
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<div class="setting-label">Username</div>
|
||||
<input type="text" class="settings-input" x-model="accountForm.login" style="max-width:300px;">
|
||||
</div>
|
||||
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<div class="setting-label">Full name</div>
|
||||
<input type="text" class="settings-input" x-model="accountForm.full_name" style="max-width:300px;">
|
||||
</div>
|
||||
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<div class="setting-label">Email</div>
|
||||
<input type="email" class="settings-input" x-model="accountForm.email" style="max-width:300px;">
|
||||
</div>
|
||||
<div style="margin-top:12px;">
|
||||
<button class="btn btn-primary" @click="saveAccount()" :disabled="accountSaving" style="width:auto;padding:8px 20px;font-size:13px;">
|
||||
<span x-show="!accountSaving">💾 Save changes</span>
|
||||
<span x-show="accountSaving">Saving…</span>
|
||||
</button>
|
||||
<span x-show="accountMsg" x-text="accountMsg" style="margin-left:12px;font-size:13px;color:#0F7B6C;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password change -->
|
||||
<div class="setting-group">
|
||||
<h3>Change password</h3>
|
||||
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<div class="setting-label">New password</div>
|
||||
<div class="pw-wrapper" style="max-width:300px;">
|
||||
<input :type="pwVisible ? 'text' : 'password'" class="settings-input" x-model="accountForm.password" placeholder="Leave blank to keep current" style="padding-right:36px;">
|
||||
<button type="button" class="pw-toggle" @click="pwVisible=!pwVisible" x-text="pwVisible ? '🙈' : '👁'" style="top:50%;"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="setting-label">Email</div>
|
||||
<div class="setting-desc" x-text="userInfo.email || '—'"></div>
|
||||
</div>
|
||||
<div style="margin-top:12px;">
|
||||
<button class="btn btn-primary" @click="saveAccount()" :disabled="accountSaving || !accountForm.password" style="width:auto;padding:8px 20px;font-size:13px;">
|
||||
<span x-show="!accountSaving">🔒 Update password</span>
|
||||
<span x-show="accountSaving">Saving…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user