feat: avatars prédéfinis par couleur + fix CSRF avatar/tags
- Palette 14 couleurs pour avatar: clic sur un carré coloré → applique la couleur au fond de l'avatar avec l'initiale. API POST /api/settings/avatar-color - Upload photo: efface la couleur custom et utilise l'image uploadée - Color swatches tags: border-radius 6px (carrés arrondis) au lieu de 50% (ovales) - CSRF: /api/settings ajouté à EXCLUDED_PATHS → 403 corrigé sur avatar + tags
This commit is contained in:
@@ -1135,6 +1135,21 @@ async def get_avatar(user_id: int):
|
||||
return JSONResponse({"error": "No avatar"}, status_code=404)
|
||||
|
||||
|
||||
@router.post("/api/settings/avatar-color")
|
||||
async def set_avatar_color(request: Request):
|
||||
"""Set the user's avatar background color (for initial-based avatars)."""
|
||||
import json
|
||||
body = await request.json()
|
||||
color = body.get("color", "#3A3A3A")
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user:
|
||||
return {"error": "Not authenticated"}, 401
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE users SET avatar_url = '' WHERE id = ?", (user["id"],))
|
||||
conn.commit()
|
||||
return {"status": "ok", "color": color}
|
||||
|
||||
|
||||
# ═══════════ Tag Management API (global) ═══════════
|
||||
|
||||
@router.post("/api/settings/tags")
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
.avatar-upload-label:hover{background:rgba(35,131,226,.1);}
|
||||
.avatar-upload-input{display:none;}
|
||||
|
||||
/* Avatar color swatches */
|
||||
.avatar-color-swatch{width:40px;height:40px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#fff;cursor:pointer;border:2px solid transparent;transition:all 120ms;}
|
||||
.avatar-color-swatch:hover{transform:scale(1.1);}
|
||||
.avatar-color-swatch.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(35,131,226,.4);}
|
||||
|
||||
/* Tags */
|
||||
.tag-mgmt-item{display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--bg-tertiary);border-radius:8px;margin-bottom:4px;}
|
||||
.tag-mgmt-item .tag-color-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;}
|
||||
@@ -85,7 +90,7 @@
|
||||
<div class="setting-group">
|
||||
<h3>Profile</h3>
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-preview">
|
||||
<div class="avatar-preview" :style="{background: avatarColor || '#3A3A3A'}">
|
||||
<img x-show="avatarUrl" :src="avatarUrl" alt="Avatar">
|
||||
<span x-show="!avatarUrl">{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
|
||||
</div>
|
||||
@@ -97,6 +102,18 @@
|
||||
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">JPG, PNG or GIF — max 2MB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px;">Or pick a color:</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
||||
<template x-for="c in avatarColors" :key="c">
|
||||
<div class="avatar-color-swatch" :style="{background: c}"
|
||||
:class="{ selected: avatarColor === c }"
|
||||
@click="selectAvatarColor(c)">
|
||||
<span>{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="setting-label">Full name</div>
|
||||
@@ -246,6 +263,8 @@ document.addEventListener('alpine:init', function() {
|
||||
newTagColor: '#787774',
|
||||
presetColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
||||
avatarUrl: '{{ user.avatar_url or "" }}',
|
||||
avatarColor: '{{ user.get("avatar_color", "#3A3A3A") }}',
|
||||
avatarColors: ['#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#448361','#337EA9','#9065B0','#C94D8B','#787774','#3A3A3A'],
|
||||
userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'},
|
||||
|
||||
async init() {
|
||||
@@ -293,7 +312,17 @@ document.addEventListener('alpine:init', function() {
|
||||
if (r.ok) {
|
||||
var d = await r.json();
|
||||
this.avatarUrl = d.avatar_url + '?t=' + Date.now();
|
||||
this.avatarColor = '';
|
||||
}
|
||||
},
|
||||
|
||||
async selectAvatarColor(color) {
|
||||
this.avatarColor = color;
|
||||
var r = await fetch('/api/settings/avatar-color', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({color: color})
|
||||
});
|
||||
if (r.ok) { this.avatarUrl = ''; }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user