diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index c5bd1bc..80cfca3 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -1096,6 +1096,45 @@ async def app_settings_page(request: Request): return template.render(**ctx) +@router.post("/api/settings/avatar") +async def upload_avatar(request: Request): + """Upload a user avatar image.""" + from fastapi import UploadFile, File + from pathlib import Path + import uuid, os + form = await request.form() + file = form.get("file") + if not file: + return {"error": "No file"}, 400 + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return {"error": "Not authenticated"}, 401 + # Save to data/avatars + avatars_dir = Path("/data/avatars") + avatars_dir.mkdir(parents=True, exist_ok=True) + ext = os.path.splitext(file.filename)[1] or ".png" + filename = f"{user['id']}_{uuid.uuid4().hex[:8]}{ext}" + filepath = avatars_dir / filename + content = await file.read() + filepath.write_bytes(content) + # Update user avatar_url + avatar_url = f"/api/settings/avatar/{filename}" + with get_conn() as conn: + conn.execute("UPDATE users SET avatar_url = ? WHERE id = ?", (avatar_url, user["id"])) + conn.commit() + return {"avatar_url": avatar_url} + + +@router.get("/api/avatar/{user_id:int}") +async def get_avatar(user_id: int): + """Redirect to the user's avatar.""" + with get_conn() as conn: + row = conn.execute("SELECT avatar_url FROM users WHERE id = ?", (user_id,)).fetchone() + if row and row["avatar_url"]: + return RedirectResponse(row["avatar_url"], status_code=302) + return JSONResponse({"error": "No avatar"}, status_code=404) + + # ═══════════ Tag Management API (global) ═══════════ @router.post("/api/settings/tags") diff --git a/app/templates/settings.html b/app/templates/settings.html index 93b931a..6e2bf56 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -15,77 +15,223 @@ {% block content %} -
- -
-

🏷️ Tags Management

-

Manage all tags across your workspace. Changes apply globally.

-
- -
No tags yet
-
- -
-
-
- - -
-
- -
-

📋 Workspace

-
-
-
Workspace name
-
Current: {{ workspaceName }}
+ +
+

Notifications

+

Choose when and how you want to be notified.

+
+

Email notifications

+
+
+
Comments
+
When someone comments on your pages
+
+
Coming soon
+
+
+
+
Mentions
+
When someone @mentions you
+
+
Coming soon
+
+
+ + +
+

Workspace

+

General workspace settings.

+
+

General

+
+
+
Workspace name
+
+
+
+
+
+
Default view
+
Choose the default view for new pages
+
+
+ +
+
+
+
+ + +
+

Tags

+

Manage all tags across your workspace.

+
+

All tags

+ +
+ + + +
+
+
+ + +
+

Features

+

Integrations and feature toggles.

+
+

Integrations

+
+
+
Gitea
+
Connected to git.dracodev.net
+
+
✅ Active
+
+
+
+
GitHub
+
Connect your GitHub account
+
+
Coming soon
+
+
+
+ + +
+

Administration

+

Workspace administration and security.

+
+

Security

+
+
+
Session timeout
+
Automatically log out after inactivity
+
+
30 days
+
+
+
+
Two-factor authentication
+
Add an extra layer of security
+
+
Coming soon
+
+
+
+
@@ -94,10 +240,13 @@ document.addEventListener('alpine:init', function() { Alpine.data('settingsInit', function() { return { + activeSection: 'account', allTags: [], newTagName: '', newTagColor: '#787774', presetColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'], + avatarUrl: '{{ user.avatar_url or "" }}', + userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'}, async init() { await this.loadTags(); @@ -114,35 +263,37 @@ document.addEventListener('alpine:init', function() { async createTag() { var n = this.newTagName.trim(); if (!n) return; - try { - var r = await fetch('/api/settings/tags', { - method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({name: n, color: this.newTagColor}) - }); - if (r.ok) { - this.newTagName = ''; - this.newTagColor = '#787774'; - await this.loadTags(); - } - } catch(e) {} + var r = await fetch('/api/settings/tags', { + method: 'POST', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({name: n, color: this.newTagColor}) + }); + if (r.ok) { this.newTagName = ''; this.newTagColor = '#787774'; await this.loadTags(); } }, async updateTagColor(id, color) { - try { - await fetch('/api/settings/tags/' + id, { - method: 'PUT', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({color: color}) - }); - await this.loadTags(); - } catch(e) {} + await fetch('/api/settings/tags/' + id, { + method: 'PUT', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({color: color}) + }); + await this.loadTags(); }, async deleteTag(id) { if (!confirm('Delete this tag?')) return; - try { - await fetch('/api/settings/tags/' + id, { method: 'DELETE' }); - await this.loadTags(); - } catch(e) {} + await fetch('/api/settings/tags/' + id, { method: 'DELETE' }); + await this.loadTags(); + }, + + async uploadAvatar(ev) { + var file = ev.target.files[0]; + if (!file) return; + var form = new FormData(); + form.append('file', file); + var r = await fetch('/api/settings/avatar', { method: 'POST', body: form }); + if (r.ok) { + var d = await r.json(); + this.avatarUrl = d.avatar_url + '?t=' + Date.now(); + } } }; });