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:
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);