feat: account management panel — /accounts + /api/users/me
- Page /accounts: profil utilisateur éditable, comptes connectés, workspace members - API GET/PUT /api/users/me: lecture/écriture profil local (full_name, email) - Users list from local DB, auth via Gitea OAuth2 - Imports SessionManager clean dans api.py
This commit is contained in:
@@ -13,6 +13,7 @@ from app.config import settings
|
||||
from app.db import get_conn
|
||||
from app.routers.board import _issue_column, _map_issue_to_card, STATUS_COLORS, STATUS_LABELS
|
||||
from app.services.gitea_client import gitea
|
||||
from app.auth.session import SessionManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["api"], prefix="/api")
|
||||
@@ -538,6 +539,40 @@ async def delete_checklist(checklist_id: int):
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# ── v1.0.0: User management ──
|
||||
|
||||
@router.get("/users/me")
|
||||
async def get_my_profile(request: Request):
|
||||
"""Get current user profile."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user:
|
||||
return {"login": "guest", "full_name": "Guest", "email": ""}
|
||||
with get_conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM users WHERE login=?", (user.get("login", ""),)
|
||||
).fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return {"login": user.get("login", ""), "full_name": "", "email": ""}
|
||||
|
||||
|
||||
@router.put("/users/me")
|
||||
async def update_my_profile(request: Request, full_name: str = Query(default=""),
|
||||
email: str = Query(default="")):
|
||||
"""Update current user's local profile."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user:
|
||||
raise HTTPException(401, "Not authenticated")
|
||||
login = user.get("login", "")
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"UPDATE users SET full_name=?, email=? WHERE login=?",
|
||||
(full_name, email, login),
|
||||
)
|
||||
conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
# ── v0.5.0: Card priority & due date ──
|
||||
|
||||
@router.post("/card/{owner}/{repo}/{issue_id}")
|
||||
|
||||
@@ -82,6 +82,20 @@ async def view_page_root(request: Request, page_id: int):
|
||||
return template.render(**ctx)
|
||||
|
||||
|
||||
@router.get("/accounts", response_class=HTMLResponse)
|
||||
async def accounts_page(request: Request):
|
||||
"""Account management panel."""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request, [])
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
with get_conn() as conn:
|
||||
users = conn.execute("SELECT * FROM users ORDER BY created_at DESC").fetchall()
|
||||
ctx = {**sidebar, "user": user, "users": [dict(u) for u in users]}
|
||||
template = env.get_template("accounts.html")
|
||||
return template.render(**ctx)
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(
|
||||
request: Request,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
{% extends "base.html" %}
|
||||
{% block page_icon %}👤{% endblock %}
|
||||
{% block page_title %}Accounts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-title-area">
|
||||
<div class="page-title">
|
||||
<span class="page-icon-lg">👤</span>
|
||||
<h1>Account Management</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 24px; max-width: 900px;" x-data="accountsData()">
|
||||
<!-- Current user profile -->
|
||||
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px; margin-bottom:24px;">
|
||||
<h3 style="margin-bottom:16px;">Your Profile</h3>
|
||||
<div style="display:flex; gap:16px; align-items:flex-start;">
|
||||
<div class="workspace-avatar" style="width:48px; height:48px; font-size:20px;">
|
||||
{{ user.login[0] if user else 'B' }}
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; gap:12px; margin-bottom:12px;">
|
||||
<div style="flex:1;">
|
||||
<label style="font-size:12px; color:var(--text-dim);">Username</label>
|
||||
<input type="text" value="{{ user.login }}" disabled
|
||||
style="width:100%; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:6px;
|
||||
padding:8px; color:var(--text-dim); font-size:14px;">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label style="font-size:12px; color:var(--text-dim);">Display name</label>
|
||||
<input type="text" x-model="profile.full_name"
|
||||
style="width:100%; background:var(--bg-secondary); border:1px solid var(--border); border-radius:6px;
|
||||
padding:8px; color:var(--text-primary); font-size:14px; outline:none;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<label style="font-size:12px; color:var(--text-dim);">Email</label>
|
||||
<input type="text" x-model="profile.email"
|
||||
style="width:100%; background:var(--bg-secondary); border:1px solid var(--border); border-radius:6px;
|
||||
padding:8px; color:var(--text-primary); font-size:14px; outline:none;">
|
||||
</div>
|
||||
<button class="btn-new" @click="saveProfile()">Save Profile</button>
|
||||
<span x-show="saved" style="color:var(--green); font-size:13px; margin-left:8px;">✓ Saved</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connected accounts -->
|
||||
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px; margin-bottom:24px;">
|
||||
<h3 style="margin-bottom:12px;">Connected Accounts</h3>
|
||||
<div style="display:flex; align-items:center; gap:12px; padding:12px; border:1px solid var(--border); border-radius:6px;">
|
||||
<span style="font-size:20px;">🔗</span>
|
||||
<div style="flex:1;">
|
||||
<div style="font-weight:600;">Gitea</div>
|
||||
<div style="font-size:12px; color:var(--text-dim);">
|
||||
{% if user %}Connected as {{ user.login }}{% else %}Not connected{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if user %}
|
||||
<a href="/auth/logout" class="toolbar-btn" style="color:var(--red);">Disconnect</a>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="btn-new">Connect Gitea</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users in workspace -->
|
||||
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px;">
|
||||
<h3 style="margin-bottom:12px;">Workspace Members</h3>
|
||||
<div style="font-size:13px; color:var(--text-dim); margin-bottom:12px;">
|
||||
Members are managed through Gitea. New users connect via OAuth and appear here automatically.
|
||||
</div>
|
||||
<table class="data-table" style="width:100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Joined</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex; align-items:center; gap:8px;">
|
||||
<div class="card-avatar" style="width:24px; height:24px; font-size:11px;">{{ u.login[0] }}</div>
|
||||
<span>{{ u.login }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ u.email or '—' }}</td>
|
||||
<td>{{ u.created_at[:10] if u.created_at else '—' }}</td>
|
||||
<td>{% if u.is_admin %}Admin{% else %}Member{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not users %}
|
||||
<tr><td colspan="4" style="text-align:center; color:var(--text-dim); padding:24px;">No members yet</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function accountsData() {
|
||||
return {
|
||||
profile: { full_name: '', email: '' },
|
||||
saved: false,
|
||||
async init() {
|
||||
try {
|
||||
const r = await fetch('/api/users/me');
|
||||
const data = await r.json();
|
||||
this.profile = { full_name: data.full_name || '', email: data.email || '' };
|
||||
} catch(e) {}
|
||||
},
|
||||
saveProfile() {
|
||||
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
const token = csrf ? csrf[1] : '';
|
||||
fetch(`/api/users/me?full_name=${encodeURIComponent(this.profile.full_name)}&email=${encodeURIComponent(this.profile.email)}`, {
|
||||
method: 'PUT', headers: { 'X-CSRF-Token': token }
|
||||
}).then(() => {
|
||||
this.saved = true;
|
||||
setTimeout(() => this.saved = false, 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user