calendar

A HTML/PHP project for a self-hosted (family)-calendar.
Log | Files | Refs | README

commit 52998960b3c6d171cd9b1fba74eb79d8d363a6a8
parent b2f3f834486f1176f23d6a61e51ce875e54199a6
Author: Julian Piribauer <julian.piribauer@gmail.com>
Date:   Sat,  9 May 2026 17:49:56 +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/attendee listing endpoints
- Update nginx to forward X-Authentik-Name header from Authentik
- Frontend: topbar, member modal, and event attendee list now show display name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Diffstat:
Mapp/main.py | 62++++++++++++++++++++++++++++++++++++++++----------------------
Mapp/models.py | 7+++++++
Mnginx/calendar | 2++
Mstatic/app.js | 20++++++++++----------
4 files changed, 59 insertions(+), 32 deletions(-)

diff --git a/app/main.py b/app/main.py @@ -8,7 +8,7 @@ from datetime import datetime import os from .database import get_db, engine -from .models import Base, Calendar, CalendarMembership, PendingInvitation, Event, EventAttendee +from .models import Base, UserProfile, Calendar, CalendarMembership, PendingInvitation, Event, EventAttendee from .schemas import ( CalendarCreate, CalendarUpdate, VisibilityUpdate, InviteRequest, AddMemberRequest, EventCreate, EventUpdate, @@ -43,12 +43,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 _ensure_personal_calendar(db: Session, username: str) -> None: @@ -115,15 +132,16 @@ 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) _ensure_personal_calendar(db, username) - return {"username": username, "email": email} + return {"username": username, "email": email, "display_name": display_name} @app.get("/api/calendars") def list_calendars(request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) memberships = ( db.query(CalendarMembership) .filter(CalendarMembership.username == username) @@ -132,7 +150,7 @@ def list_calendars(request: Request, db: Session = Depends(get_db)): result = [] for m in memberships: cal = m.calendar - member_usernames = [x.username for x in cal.memberships] + members = [{"username": x.username, "display_name": _display_name(db, x.username)} for x in cal.memberships] pending_emails = [p.invitee_email for p in cal.pending_invitations] result.append( { @@ -142,7 +160,7 @@ def list_calendars(request: Request, db: Session = Depends(get_db)): "is_personal": cal.is_personal, "is_owner": cal.owner_username == username, "is_visible": m.is_visible, - "members": member_usernames, + "members": members, "pending_invitations": pending_emails if cal.owner_username == username else [], } ) @@ -151,7 +169,7 @@ def list_calendars(request: Request, db: Session = Depends(get_db)): @app.post("/api/calendars", status_code=201) def create_calendar(body: CalendarCreate, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) cal = Calendar(name=body.name, color=body.color, owner_username=username, is_personal=False) db.add(cal) db.flush() @@ -176,7 +194,7 @@ def update_calendar( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) cal = _require_owner(db, calendar_id, username) if body.name is not None: cal.name = body.name @@ -188,7 +206,7 @@ def update_calendar( @app.delete("/api/calendars/{calendar_id}", status_code=204) def delete_calendar(calendar_id: int, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) cal = _require_owner(db, calendar_id, username) if cal.is_personal: raise HTTPException(status_code=400, detail="Cannot delete personal calendar") @@ -203,7 +221,7 @@ def set_visibility( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) m = ( db.query(CalendarMembership) .filter( @@ -226,7 +244,7 @@ def invite_member( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) _require_owner(db, calendar_id, username) email = body.email.strip().lower() try: @@ -245,7 +263,7 @@ def add_member( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) _require_owner(db, calendar_id, username) target = body.username.strip() if not target: @@ -258,7 +276,7 @@ def add_member( except IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="User is already a member") - return {"username": target} + return {"username": target, "display_name": _display_name(db, target)} @app.delete("/api/calendars/{calendar_id}/members/{member_username}", status_code=204) @@ -268,7 +286,7 @@ def remove_member( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) cal = _require_owner(db, calendar_id, username) if member_username == username: raise HTTPException(status_code=400, detail="Cannot remove yourself as owner") @@ -292,7 +310,7 @@ def revoke_invitation( request: Request, db: Session = Depends(get_db), ): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) _require_owner(db, calendar_id, username) inv = ( db.query(PendingInvitation) @@ -309,7 +327,7 @@ def revoke_invitation( @app.delete("/api/calendars/{calendar_id}/leave", status_code=204) def leave_calendar(calendar_id: int, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) cal = _require_member(db, calendar_id, username) if cal.owner_username == username: raise HTTPException(status_code=400, detail="Owner cannot leave; delete the calendar instead") @@ -337,7 +355,7 @@ def _parse_dt(s: str) -> datetime: @app.get("/api/events") def list_events(year: int, month: int, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) memberships = db.query(CalendarMembership).filter(CalendarMembership.username == username).all() cal_ids = [m.calendar_id for m in memberships] @@ -366,7 +384,7 @@ def list_events(year: int, month: int, request: Request, db: Session = Depends(g @app.post("/api/events", status_code=201) def create_event(body: EventCreate, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) _require_member(db, body.calendar_id, username) start = _parse_dt(body.start_dt) end = _parse_dt(body.end_dt) @@ -401,7 +419,7 @@ def create_event(body: EventCreate, request: Request, db: Session = Depends(get_ @app.patch("/api/events/{event_id}") def update_event(event_id: int, body: EventUpdate, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) ev = db.query(Event).filter(Event.id == event_id).first() if not ev: raise HTTPException(status_code=404) @@ -439,7 +457,7 @@ def update_event(event_id: int, body: EventUpdate, request: Request, db: Session @app.delete("/api/events/{event_id}", status_code=204) def delete_event(event_id: int, request: Request, db: Session = Depends(get_db)): - username, _ = _get_identity(request) + username, _, _dn = _get_identity(request) ev = db.query(Event).filter(Event.id == event_id).first() if not ev: raise HTTPException(status_code=404) 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 Calendar(Base): __tablename__ = "calendars" diff --git a/nginx/calendar b/nginx/calendar @@ -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 @@ -42,7 +42,7 @@ async function init() { const me = await api('GET', '/api/me'); state.username = me.username; state.email = me.email; - document.getElementById('username-display').textContent = me.username; + document.getElementById('username-display').textContent = me.display_name || me.username; await loadCalendars(); await fetchEvents(); @@ -565,12 +565,12 @@ function renderMembersModal() { const cal = state.calendars.find(c => c.id === _membersCalId); if (!cal) return; document.getElementById('modal-members-title').textContent = cal.name; - document.getElementById('cal-members-list').innerHTML = cal.members.map(uname => ` + document.getElementById('cal-members-list').innerHTML = cal.members.map(m => ` <div class="member-row"> - <span class="member-username">${escHtml(uname)}</span> - ${uname === state.username ? `<span style="font-size:0.7rem;opacity:0.4;text-transform:uppercase;letter-spacing:0.06em">owner</span>` : ''} - ${cal.is_owner && uname !== state.username ? ` - <button class="btn-icon danger" title="${t('remove')}" onclick="removeMember(${cal.id},'${escHtml(uname)}')"> + <span class="member-username">${escHtml(m.display_name || m.username)}</span> + ${m.username === state.username ? `<span style="font-size:0.7rem;opacity:0.4;text-transform:uppercase;letter-spacing:0.06em">owner</span>` : ''} + ${cal.is_owner && m.username !== state.username ? ` + <button class="btn-icon danger" title="${t('remove')}" onclick="removeMember(${cal.id},'${escHtml(m.username)}')"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg> </button>` : ''} </div>`).join(''); @@ -665,13 +665,13 @@ function updateEventAttendees(existingEvent) { const list = document.createElement('div'); list.className = 'attendee-list'; - cal.members.forEach(uname => { + cal.members.forEach(m => { const label = document.createElement('label'); label.className = 'attendee-item'; const cb = document.createElement('input'); - cb.type = 'checkbox'; cb.value = uname; - cb.checked = existingEvent ? existingEvent.attendees.includes(uname) : true; - label.append(cb, document.createTextNode(uname)); + cb.type = 'checkbox'; cb.value = m.username; + cb.checked = existingEvent ? existingEvent.attendees.includes(m.username) : true; + label.append(cb, document.createTextNode(m.display_name || m.username)); list.appendChild(label); }); cont.appendChild(list);