familytree

A self-hosted family tree builder.
Log | Files | Refs | README

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:
Mapp/main.py | 70++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mapp/models.py | 7+++++++
Mnginx/family | 2++
Mstatic/app.js | 4++--
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)}')">