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)
|
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) ═══════════
|
# ═══════════ Tag Management API (global) ═══════════
|
||||||
|
|
||||||
@router.post("/api/settings/tags")
|
@router.post("/api/settings/tags")
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
.avatar-upload-label:hover{background:rgba(35,131,226,.1);}
|
.avatar-upload-label:hover{background:rgba(35,131,226,.1);}
|
||||||
.avatar-upload-input{display:none;}
|
.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 */
|
/* 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{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;}
|
.tag-mgmt-item .tag-color-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;}
|
||||||
@@ -85,7 +90,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<h3>Profile</h3>
|
<h3>Profile</h3>
|
||||||
<div class="avatar-section">
|
<div class="avatar-section">
|
||||||
<div class="avatar-preview">
|
<div class="avatar-preview" :style="{background: avatarColor || '#3A3A3A'}">
|
||||||
<img x-show="avatarUrl" :src="avatarUrl" alt="Avatar">
|
<img x-show="avatarUrl" :src="avatarUrl" alt="Avatar">
|
||||||
<span x-show="!avatarUrl">{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
|
<span x-show="!avatarUrl">{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
|
||||||
</div>
|
</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 style="font-size:11px;color:var(--text-dim);margin-top:4px;">JPG, PNG or GIF — max 2MB</div>
|
||||||
</div>
|
</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 class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="setting-label">Full name</div>
|
<div class="setting-label">Full name</div>
|
||||||
@@ -246,6 +263,8 @@ document.addEventListener('alpine:init', function() {
|
|||||||
newTagColor: '#787774',
|
newTagColor: '#787774',
|
||||||
presetColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
presetColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
||||||
avatarUrl: '{{ user.avatar_url or "" }}',
|
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 "" }}'},
|
userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -293,7 +312,17 @@ document.addEventListener('alpine:init', function() {
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
this.avatarUrl = d.avatar_url + '?t=' + Date.now();
|
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