fix: avatars persistant — route fichier + couleur en DB + sidebar affichage
- Route manquante GET /api/settings/avatar/{filename} → sert le fichier uploadé
(résout le 404 sur l'image uploadée)
- Colonne avatar_color ajoutée à users (migration ALTER TABLE try/except)
- set_avatar_color sauvegarde la couleur dans la DB (pas seulement local)
- _sidebar_data() lit avatar_url + avatar_color depuis la DB
- Sidebar avatar: fond coloré quand pas d'image, image quand uploadée
avec background-image:url() + initiale en fallback
This commit is contained in:
@@ -27,6 +27,7 @@ def init_db():
|
|||||||
full_name TEXT NOT NULL DEFAULT '',
|
full_name TEXT NOT NULL DEFAULT '',
|
||||||
email TEXT NOT NULL DEFAULT '',
|
email TEXT NOT NULL DEFAULT '',
|
||||||
avatar_url TEXT NOT NULL DEFAULT '',
|
avatar_url TEXT NOT NULL DEFAULT '',
|
||||||
|
avatar_color TEXT NOT NULL DEFAULT '#3A3A3A',
|
||||||
password_hash TEXT,
|
password_hash TEXT,
|
||||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||||
is_active INTEGER NOT NULL DEFAULT 1,
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
@@ -316,6 +317,11 @@ def init_db():
|
|||||||
conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)")
|
conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass
|
pass
|
||||||
|
# Migration: add avatar_color (v2.3.0+)
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE users ADD COLUMN avatar_color TEXT NOT NULL DEFAULT '#3A3A3A'")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
# Migration: add sort_order for drag & drop tree reordering (v1.1.0+)
|
# Migration: add sort_order for drag & drop tree reordering (v1.1.0+)
|
||||||
try:
|
try:
|
||||||
conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0")
|
conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|||||||
ws = user.get("login", "Bruno") if user else "Bruno"
|
ws = user.get("login", "Bruno") if user else "Bruno"
|
||||||
initial = ws[0].upper() if ws else "B"
|
initial = ws[0].upper() if ws else "B"
|
||||||
|
|
||||||
|
# Get avatar info from DB
|
||||||
|
avatar_url = ""
|
||||||
|
avatar_color = "#3A3A3A"
|
||||||
|
if user:
|
||||||
|
try:
|
||||||
|
with get_conn() as conn:
|
||||||
|
row = conn.execute("SELECT avatar_url, avatar_color FROM users WHERE id = ?", (user["id"],)).fetchone()
|
||||||
|
if row:
|
||||||
|
avatar_url = row["avatar_url"] or ""
|
||||||
|
avatar_color = row["avatar_color"] or "#3A3A3A"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
recent_pages = []
|
recent_pages = []
|
||||||
for repo in repos[:10]:
|
for repo in repos[:10]:
|
||||||
full_name = repo.get("full_name", "")
|
full_name = repo.get("full_name", "")
|
||||||
@@ -76,6 +89,8 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|||||||
"favorite_pages": [],
|
"favorite_pages": [],
|
||||||
"shared_pages": [],
|
"shared_pages": [],
|
||||||
"user": user,
|
"user": user,
|
||||||
|
"avatar_url": avatar_url,
|
||||||
|
"avatar_color": avatar_color,
|
||||||
"workspace_key": "",
|
"workspace_key": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1125,6 +1140,17 @@ async def upload_avatar(request: Request):
|
|||||||
return {"avatar_url": avatar_url}
|
return {"avatar_url": avatar_url}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/settings/avatar/{filename:path}")
|
||||||
|
async def serve_avatar_file(filename: str):
|
||||||
|
"""Serve an uploaded avatar image file."""
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from pathlib import Path
|
||||||
|
filepath = Path("/data/avatars") / filename
|
||||||
|
if not filepath.is_file():
|
||||||
|
return JSONResponse({"error": "Not found"}, status_code=404)
|
||||||
|
return FileResponse(filepath)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/avatar/{user_id:int}")
|
@router.get("/api/avatar/{user_id:int}")
|
||||||
async def get_avatar(user_id: int):
|
async def get_avatar(user_id: int):
|
||||||
"""Redirect to the user's avatar."""
|
"""Redirect to the user's avatar."""
|
||||||
@@ -1137,15 +1163,14 @@ async def get_avatar(user_id: int):
|
|||||||
|
|
||||||
@router.post("/api/settings/avatar-color")
|
@router.post("/api/settings/avatar-color")
|
||||||
async def set_avatar_color(request: Request):
|
async def set_avatar_color(request: Request):
|
||||||
"""Set the user's avatar background color (for initial-based avatars)."""
|
"""Set the user's avatar background color."""
|
||||||
import json
|
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
color = body.get("color", "#3A3A3A")
|
color = body.get("color", "#3A3A3A")
|
||||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
if not user:
|
if not user:
|
||||||
return {"error": "Not authenticated"}, 401
|
return {"error": "Not authenticated"}, 401
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
conn.execute("UPDATE users SET avatar_url = '' WHERE id = ?", (user["id"],))
|
conn.execute("UPDATE users SET avatar_url = '', avatar_color = ? WHERE id = ?", (color, user["id"]))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return {"status": "ok", "color": color}
|
return {"status": "ok", "color": color}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<div class="sidebar-workspace-header" @click="workspaceMenuOpen = !workspaceMenuOpen" @mouseenter="wsHeaderHover=true" @mouseleave="wsHeaderHover=false"
|
<div class="sidebar-workspace-header" @click="workspaceMenuOpen = !workspaceMenuOpen" @mouseenter="wsHeaderHover=true" @mouseleave="wsHeaderHover=false"
|
||||||
:class="{ hover: wsHeaderHover || workspaceMenuOpen }">
|
:class="{ hover: wsHeaderHover || workspaceMenuOpen }">
|
||||||
<div class="workspace-info">
|
<div class="workspace-info">
|
||||||
<div class="workspace-avatar">{{ workspaceInitial }}</div>
|
<div class="workspace-avatar" style="{% if avatar_url %}background-image:url('{{ avatar_url }}');background-size:cover;{% else %}background:{{ avatar_color }};{% endif %}">{% if not avatar_url %}{{ user.full_name[:1].upper() if user and user.get('full_name') else workspaceInitial }}{% endif %}</div>
|
||||||
<span class="workspace-name">{{ user.full_name or user.login or workspaceName }}</span>
|
<span class="workspace-name">{{ user.full_name or user.login or workspaceName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span>
|
<span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user