feat: tags personnalisés par utilisateur (user_id)
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

DB:
- tags.user_id REFERENCES users(id)
- UNIQUE(name, user_id) au lieu de UNIQUE(name)
- Migration v2.6: alter + recreate table

API (tous les endpoints tags):
- POST/PUT/DELETE /api/settings/tags: filtré par user_id
- GET /api/settings/tags/all: retourne uniquement les tags du user
- GET/POST /api/local-workspace/*/tags: scope user

→ Les tags de l'utilisateur 1 ne sont pas visibles par l'utilisateur 2
This commit is contained in:
2026-07-13 13:58:13 -04:00
parent f262076545
commit 5d1857941e
2 changed files with 46 additions and 25 deletions
+17 -2
View File
@@ -306,9 +306,11 @@ def init_db():
-- v2.2.0: Tags system
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
color TEXT DEFAULT '#787774',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(name, user_id)
);
CREATE TABLE IF NOT EXISTS page_tags (
@@ -379,6 +381,19 @@ def init_db():
)""")
except sqlite3.OperationalError:
pass
# v2.6: Tags per-user
try:
conn.execute("ALTER TABLE tags ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id)")
except sqlite3.OperationalError:
pass
try:
# Recreate UNIQUE constraint for per-user tags
conn.execute("CREATE TABLE IF NOT EXISTS tags_new (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, color TEXT DEFAULT '#787774', user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(name, user_id))")
conn.execute("INSERT OR IGNORE INTO tags_new (id, name, color, user_id, created_at) SELECT id, name, color, COALESCE(user_id,0), created_at FROM tags")
conn.execute("DROP TABLE tags")
conn.execute("ALTER TABLE tags_new RENAME TO tags")
except sqlite3.OperationalError:
pass
conn.commit()
+29 -23
View File
@@ -1182,82 +1182,87 @@ async def set_avatar_color(request: Request):
return {"status": "ok", "color": color}
# ═══════════ Tag Management API (global) ═══════════
# ═══════════ Tag Management API (per-user) ═══════════
@router.post("/api/settings/tags")
async def create_tag_global(request: Request):
"""Create a tag with optional color."""
"""Create a tag for the current user."""
import json
body = await request.json()
tag_name = body.get("name", "").strip().lower()
color = body.get("color", "#787774")
if not tag_name:
uid = _get_user_id(request)
if not tag_name or not uid:
return {"error": "Tag name required"}, 400
with get_conn() as conn:
tag = conn.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)).fetchone()
tag = conn.execute("SELECT id FROM tags WHERE name = ? AND user_id = ?", (tag_name, uid)).fetchone()
if tag:
conn.execute("UPDATE tags SET color = ? WHERE id = ?", (color, tag["id"]))
conn.commit()
return {"tag": {"id": tag["id"], "name": tag_name, "color": color}}
cursor = conn.execute("INSERT INTO tags (name, color) VALUES (?, ?)", (tag_name, color))
cursor = conn.execute("INSERT INTO tags (name, color, user_id) VALUES (?, ?, ?)", (tag_name, color, uid))
conn.commit()
return {"tag": {"id": cursor.lastrowid, "name": tag_name, "color": color}}
@router.put("/api/settings/tags/{tag_id:int}")
async def update_tag_global(tag_id: int, request: Request):
"""Update a tag (name or color)."""
"""Update a tag (name or color) — only if owned by user."""
import json
body = await request.json()
uid = _get_user_id(request)
with get_conn() as conn:
if "name" in body:
conn.execute("UPDATE tags SET name = ? WHERE id = ?", (body["name"].strip().lower(), tag_id))
conn.execute("UPDATE tags SET name = ? WHERE id = ? AND user_id = ?", (body["name"].strip().lower(), tag_id, uid))
if "color" in body:
conn.execute("UPDATE tags SET color = ? WHERE id = ?", (body["color"], tag_id))
conn.execute("UPDATE tags SET color = ? WHERE id = ? AND user_id = ?", (body["color"], tag_id, uid))
conn.commit()
return {"status": "ok"}
@router.delete("/api/settings/tags/{tag_id:int}")
async def delete_tag_global(tag_id: int):
"""Delete a tag and its page associations."""
async def delete_tag_global(tag_id: int, request: Request):
"""Delete a tag — only if owned by user."""
uid = _get_user_id(request)
with get_conn() as conn:
conn.execute("DELETE FROM page_tags WHERE tag_id = ?", (tag_id,))
conn.execute("DELETE FROM tags WHERE id = ?", (tag_id,))
conn.execute("DELETE FROM tags WHERE id = ? AND user_id = ?", (tag_id, uid))
conn.commit()
return {"status": "ok"}
@router.get("/api/settings/tags/all")
async def list_all_tags_global():
"""List all tags with counts."""
async def list_all_tags_global(request: Request):
"""List current user's tags with counts."""
uid = _get_user_id(request)
with get_conn() as conn:
rows = conn.execute(
"SELECT t.id, t.name, t.color, COUNT(pt.page_id) as count "
"FROM tags t LEFT JOIN page_tags pt ON pt.tag_id = t.id "
"GROUP BY t.id ORDER BY t.name"
"WHERE t.user_id = ? GROUP BY t.id ORDER BY t.name",
(uid,),
).fetchall()
return {"tags": [dict(r) for r in rows]}
# ═══════════ Tags API ═══════════
# ═══════════ Workspace Tags API ═══════════
@router.get("/api/local-workspace/tags")
async def list_tags(request: Request):
"""List all tags used in the active workspace."""
"""List tags used in the active workspace (current user's tags only)."""
ws = _get_active_workspace(request)
ws_id = ws["id"] if ws else None
uid = _get_user_id(request)
if not ws_id:
return {"tags": []}
import json
with get_conn() as conn:
rows = conn.execute(
"SELECT DISTINCT t.id, t.name, t.color, COUNT(pt.page_id) as count "
"FROM tags t "
"JOIN page_tags pt ON pt.tag_id = t.id "
"JOIN pages p ON p.id = pt.page_id AND p.workspace_id = ? "
"GROUP BY t.id ORDER BY t.name",
(ws_id,),
"WHERE t.user_id = ? GROUP BY t.id ORDER BY t.name",
(ws_id, uid),
).fetchall()
return {"tags": [dict(r) for r in rows]}
@@ -1277,20 +1282,21 @@ async def get_item_tags(item_id: int):
@router.post("/api/local-workspace/items/{item_id:int}/tags")
async def add_item_tag(request: Request, item_id: int):
"""Add a tag to an item (creates tag if new)."""
"""Add a tag to an item (creates tag if new, scoped to user)."""
import json
body = await request.json()
tag_name = body.get("name", "").strip().lower()
tag_color = body.get("color", "#787774")
uid = _get_user_id(request)
if not tag_name:
return {"error": "Tag name required"}, 400
with get_conn() as conn:
# Get or create tag
tag = conn.execute("SELECT id, name, color FROM tags WHERE name = ?", (tag_name,)).fetchone()
# Get or create tag (per user)
tag = conn.execute("SELECT id, name, color FROM tags WHERE name = ? AND user_id = ?", (tag_name, uid)).fetchone()
if not tag:
cursor = conn.execute(
"INSERT INTO tags (name, color) VALUES (?, ?)", (tag_name, tag_color)
"INSERT INTO tags (name, color, user_id) VALUES (?, ?, ?)", (tag_name, tag_color, uid)
)
conn.commit()
tag_id = cursor.lastrowid