commit 17cd600fd4e17eb1ab2cc0d01dbd288507d46357
parent 8f244e00bf1eaf2ae4ea20490d95c1ce69a8428c
Author: Julian Piribauer <julian.piribauer@gmail.com>
Date: Sat, 9 May 2026 17:49:50 +0000
Show Authentik display name instead of userid throughout
- Add UserProfile table to store username → display_name mapping
- Upsert display name on every /api/me call from X-Authentik-Name header
- Return display_name from /api/me and member listing endpoints
- Update nginx to forward X-Authentik-Name header from Authentik
- Frontend: topbar and member modal now show display name (falls back to userid if not set)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat:
4 files changed, 55 insertions(+), 28 deletions(-)
diff --git a/app/main.py b/app/main.py
@@ -6,7 +6,7 @@ from sqlalchemy.exc import IntegrityError
import os
from .database import get_db, engine
-from .models import Base, Tree, TreeMembership, PendingInvitation, Person, Partnership, ParentLink
+from .models import Base, UserProfile, Tree, TreeMembership, PendingInvitation, Person, Partnership, ParentLink
from .schemas import (
TreeCreate, TreeRename, InviteRequest, RoleUpdate, AddMemberRequest,
PersonCreate, PersonUpdate,
@@ -32,12 +32,29 @@ async def no_cache_static(request: Request, call_next):
# ── Auth helpers ──────────────────────────────────────────────────────────────
-def _get_identity(request: Request) -> tuple[str, str]:
+def _get_identity(request: Request) -> tuple[str, str, str]:
username = request.headers.get("x-authentik-username", "")
email = request.headers.get("x-authentik-email", "")
+ display_name = request.headers.get("x-authentik-name", "") or username
if not username:
raise HTTPException(status_code=401, detail="Not authenticated")
- return username, email
+ return username, email, display_name
+
+
+def _upsert_display_name(db: Session, username: str, display_name: str) -> None:
+ profile = db.query(UserProfile).filter(UserProfile.username == username).first()
+ if profile:
+ if profile.display_name != display_name:
+ profile.display_name = display_name
+ db.commit()
+ else:
+ db.add(UserProfile(username=username, display_name=display_name))
+ db.commit()
+
+
+def _display_name(db: Session, username: str) -> str:
+ profile = db.query(UserProfile).filter(UserProfile.username == username).first()
+ return profile.display_name if profile and profile.display_name else username
def _accept_pending_invitations(db: Session, username: str, email: str) -> None:
@@ -127,19 +144,20 @@ async def root():
@app.get("/api/me")
def get_me(request: Request, db: Session = Depends(get_db)):
- username, email = _get_identity(request)
+ username, email, display_name = _get_identity(request)
+ _upsert_display_name(db, username, display_name)
_accept_pending_invitations(db, username, email)
- return {"username": username, "email": email}
+ return {"username": username, "email": email, "display_name": display_name}
@app.get("/api/trees")
def list_trees(request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
memberships = db.query(TreeMembership).filter(TreeMembership.username == username).all()
result = []
for m in memberships:
tree = m.tree
- members = [{"username": x.username, "role": x.role} for x in tree.memberships]
+ members = [{"username": x.username, "role": x.role, "display_name": _display_name(db, x.username)} for x in tree.memberships]
pending = [{"email": p.invitee_email, "role": p.role} for p in tree.pending_invitations]
result.append({
"id": tree.id,
@@ -154,7 +172,7 @@ def list_trees(request: Request, db: Session = Depends(get_db)):
@app.post("/api/trees", status_code=201)
def create_tree(body: TreeCreate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
tree = Tree(name=body.name, owner_username=username)
db.add(tree)
db.flush()
@@ -166,7 +184,7 @@ def create_tree(body: TreeCreate, request: Request, db: Session = Depends(get_db
@app.patch("/api/trees/{tree_id}")
def rename_tree(tree_id: int, body: TreeRename, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
tree = _require_owner(db, tree_id, username)
tree.name = body.name
db.commit()
@@ -175,7 +193,7 @@ def rename_tree(tree_id: int, body: TreeRename, request: Request, db: Session =
@app.delete("/api/trees/{tree_id}", status_code=204)
def delete_tree(tree_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
tree = _require_owner(db, tree_id, username)
db.delete(tree)
db.commit()
@@ -183,7 +201,7 @@ def delete_tree(tree_id: int, request: Request, db: Session = Depends(get_db)):
@app.post("/api/trees/{tree_id}/invite", status_code=201)
def invite_member(tree_id: int, body: InviteRequest, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_owner(db, tree_id, username)
email = body.email.strip().lower()
if body.role not in ("editor", "viewer"):
@@ -199,7 +217,7 @@ def invite_member(tree_id: int, body: InviteRequest, request: Request, db: Sessi
@app.delete("/api/trees/{tree_id}/invitations/{email}", status_code=204)
def revoke_invitation(tree_id: int, email: str, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_owner(db, tree_id, username)
inv = db.query(PendingInvitation).filter(
PendingInvitation.tree_id == tree_id,
@@ -212,7 +230,7 @@ def revoke_invitation(tree_id: int, email: str, request: Request, db: Session =
@app.post("/api/trees/{tree_id}/members", status_code=201)
def add_member(tree_id: int, body: AddMemberRequest, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_owner(db, tree_id, username)
target = body.username.strip()
if not target:
@@ -227,12 +245,12 @@ def add_member(tree_id: int, body: AddMemberRequest, request: Request, db: Sessi
except IntegrityError:
db.rollback()
raise HTTPException(status_code=409, detail="User is already a member")
- return {"username": target, "role": body.role}
+ return {"username": target, "role": body.role, "display_name": _display_name(db, target)}
@app.patch("/api/trees/{tree_id}/members/{member_username}")
def update_member_role(tree_id: int, member_username: str, body: RoleUpdate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_owner(db, tree_id, username)
if body.role not in ("editor", "viewer"):
raise HTTPException(status_code=422, detail="role must be editor or viewer")
@@ -251,7 +269,7 @@ def update_member_role(tree_id: int, member_username: str, body: RoleUpdate, req
@app.delete("/api/trees/{tree_id}/members/{member_username}", status_code=204)
def remove_member(tree_id: int, member_username: str, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_owner(db, tree_id, username)
if member_username == username:
raise HTTPException(status_code=400, detail="Cannot remove yourself as owner")
@@ -266,7 +284,7 @@ def remove_member(tree_id: int, member_username: str, request: Request, db: Sess
@app.delete("/api/trees/{tree_id}/leave", status_code=204)
def leave_tree(tree_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
tree, m = _require_member(db, tree_id, username)
if tree.owner_username == username:
raise HTTPException(status_code=400, detail="Owner cannot leave; delete the tree instead")
@@ -278,7 +296,7 @@ def leave_tree(tree_id: int, request: Request, db: Session = Depends(get_db)):
@app.get("/api/trees/{tree_id}/graph")
def get_graph(tree_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_member(db, tree_id, username)
people = db.query(Person).filter(Person.tree_id == tree_id).all()
partnerships = db.query(Partnership).filter(Partnership.tree_id == tree_id).all()
@@ -295,7 +313,7 @@ def get_graph(tree_id: int, request: Request, db: Session = Depends(get_db)):
@app.post("/api/trees/{tree_id}/people", status_code=201)
def add_person(tree_id: int, body: PersonCreate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
p = Person(tree_id=tree_id, **body.model_dump())
db.add(p)
@@ -306,7 +324,7 @@ def add_person(tree_id: int, body: PersonCreate, request: Request, db: Session =
@app.patch("/api/trees/{tree_id}/people/{person_id}")
def update_person(tree_id: int, person_id: int, body: PersonUpdate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
p = db.query(Person).filter(Person.id == person_id, Person.tree_id == tree_id).first()
if not p:
@@ -320,7 +338,7 @@ def update_person(tree_id: int, person_id: int, body: PersonUpdate, request: Req
@app.delete("/api/trees/{tree_id}/people/{person_id}", status_code=204)
def delete_person(tree_id: int, person_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
p = db.query(Person).filter(Person.id == person_id, Person.tree_id == tree_id).first()
if not p:
@@ -337,7 +355,7 @@ def delete_person(tree_id: int, person_id: int, request: Request, db: Session =
@app.post("/api/trees/{tree_id}/partnerships", status_code=201)
def add_partnership(tree_id: int, body: PartnershipCreate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
for pid in (body.person1_id, body.person2_id):
if not db.query(Person).filter(Person.id == pid, Person.tree_id == tree_id).first():
@@ -362,7 +380,7 @@ def add_partnership(tree_id: int, body: PartnershipCreate, request: Request, db:
@app.patch("/api/trees/{tree_id}/partnerships/{partnership_id}")
def update_partnership(tree_id: int, partnership_id: int, body: PartnershipUpdate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
ps = db.query(Partnership).filter(Partnership.id == partnership_id, Partnership.tree_id == tree_id).first()
if not ps:
@@ -375,7 +393,7 @@ def update_partnership(tree_id: int, partnership_id: int, body: PartnershipUpdat
@app.delete("/api/trees/{tree_id}/partnerships/{partnership_id}", status_code=204)
def delete_partnership(tree_id: int, partnership_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
ps = db.query(Partnership).filter(Partnership.id == partnership_id, Partnership.tree_id == tree_id).first()
if not ps:
@@ -388,7 +406,7 @@ def delete_partnership(tree_id: int, partnership_id: int, request: Request, db:
@app.post("/api/trees/{tree_id}/parentlinks", status_code=201)
def add_parent_link(tree_id: int, body: ParentLinkCreate, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
if (body.partnership_id is None) == (body.parent_id is None):
raise HTTPException(status_code=422, detail="Provide exactly one of partnership_id or parent_id")
@@ -409,7 +427,7 @@ def add_parent_link(tree_id: int, body: ParentLinkCreate, request: Request, db:
@app.delete("/api/trees/{tree_id}/parentlinks/{link_id}", status_code=204)
def delete_parent_link(tree_id: int, link_id: int, request: Request, db: Session = Depends(get_db)):
- username, _ = _get_identity(request)
+ username, _, _dn = _get_identity(request)
_require_editor(db, tree_id, username)
pl = db.query(ParentLink).filter(ParentLink.id == link_id).first()
if not pl:
diff --git a/app/models.py b/app/models.py
@@ -3,6 +3,13 @@ from sqlalchemy.orm import relationship
from .database import Base
+class UserProfile(Base):
+ __tablename__ = "user_profiles"
+
+ username = Column(String, primary_key=True)
+ display_name = Column(String, nullable=False, default="")
+
+
class Tree(Base):
__tablename__ = "trees"
diff --git a/nginx/family b/nginx/family
@@ -33,11 +33,13 @@ server {
auth_request_set $auth_cookie $upstream_http_set_cookie;
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
+ auth_request_set $authentik_name $upstream_http_x_authentik_name;
add_header Set-Cookie $auth_cookie;
proxy_set_header X-Authentik-Username $authentik_username;
proxy_set_header X-Authentik-Email $authentik_email;
+ proxy_set_header X-Authentik-Name $authentik_name;
}
listen 443 ssl;
diff --git a/static/app.js b/static/app.js
@@ -46,7 +46,7 @@ async function boot() {
const me = await api('GET', '/api/me');
state.username = me.username;
state.email = me.email;
- document.getElementById('topbarUsername').textContent = me.username;
+ document.getElementById('topbarUsername').textContent = me.display_name || me.username;
await loadTrees();
} catch(e) {
showError('Failed to load: ' + e.message);
@@ -1288,7 +1288,7 @@ function renderMembersModal(treeId) {
document.getElementById('membersList').innerHTML = tree.members.map(m => `
<div class="member-row">
- <span class="member-username">${esc(m.username)}</span>
+ <span class="member-username">${esc(m.display_name || m.username)}</span>
<span class="member-role">${m.role}</span>
${isOwner && m.role !== 'owner' ? `
<button class="btn-icon danger" title="${t('remove')}" onclick="removeMember(${treeId},'${esc(m.username)}')">