From 5d1857941e7e3da36c7ef28744b5974991d0d233 Mon Sep 17 00:00:00 2001 From: bruno Date: Mon, 13 Jul 2026 13:58:13 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20tags=20personnalis=C3=A9s=20par=20utili?= =?UTF-8?q?sateur=20(user=5Fid)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/db.py | 19 +++++++++++++-- app/routers/dashboard.py | 52 ++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/db.py b/app/db.py index 2946ce4..aa83900 100644 --- a/app/db.py +++ b/app/db.py @@ -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() diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index fca55a4..38c79fa 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -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