ui: login page — SSO button + oidc_error banner

This commit is contained in:
2026-05-05 13:40:13 +01:00
parent c62d7d3ac3
commit 885439b048
4 changed files with 50 additions and 3 deletions
+12 -1
View File
@@ -922,7 +922,14 @@ func (s *Server) handleUILoginGet(w stdhttp.ResponseWriter, r *stdhttp.Request)
stdhttp.Redirect(w, r, "/", stdhttp.StatusSeeOther) stdhttp.Redirect(w, r, "/", stdhttp.StatusSeeOther)
return return
} }
view := ui.ViewData{Version: s.version()} view := ui.ViewData{
Version: s.version(),
OIDCError: r.URL.Query().Get("oidc_error"),
}
if s.deps.OIDC != nil {
view.OIDCEnabled = true
view.OIDCDisplayName = s.deps.OIDC.DisplayName()
}
if err := s.deps.UI.Render(w, "login", view); err != nil { if err := s.deps.UI.Render(w, "login", view); err != nil {
slog.Error("ui: render login", "err", err) slog.Error("ui: render login", "err", err)
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
@@ -948,6 +955,10 @@ func (s *Server) handleUILoginPost(w stdhttp.ResponseWriter, r *stdhttp.Request)
Username: username, Username: username,
Error: "Invalid username or password.", Error: "Invalid username or password.",
} }
if s.deps.OIDC != nil {
view.OIDCEnabled = true
view.OIDCDisplayName = s.deps.OIDC.DisplayName()
}
w.WriteHeader(stdhttp.StatusUnauthorized) w.WriteHeader(stdhttp.StatusUnauthorized)
if err := s.deps.UI.Render(w, "login", view); err != nil { if err := s.deps.UI.Render(w, "login", view); err != nil {
slog.Error("ui: render login (post-fail)", "err", err) slog.Error("ui: render login (post-fail)", "err", err)
+13
View File
@@ -56,6 +56,19 @@ type ViewData struct {
// today; other pages can adopt the same field. // today; other pages can adopt the same field.
Error string Error string
// OIDCEnabled is true when the server has an OIDC provider
// configured. The login page uses it to show the SSO button.
OIDCEnabled bool
// OIDCDisplayName is the human-readable label for the OIDC
// provider (e.g. "Authelia"). Shown on the SSO button.
OIDCDisplayName string
// OIDCError holds an error code returned via ?oidc_error=… after
// a failed OIDC callback. The login page maps it to a user-facing
// message.
OIDCError string
// Page carries page-specific data. Concrete type is the page's // Page carries page-specific data. Concrete type is the page's
// own struct. // own struct.
Page any Page any
File diff suppressed because one or more lines are too long
+24 -1
View File
@@ -10,6 +10,18 @@
<h2 class="text-lg font-medium tracking-[-0.005em] text-center mb-7">Sign in to continue</h2> <h2 class="text-lg font-medium tracking-[-0.005em] text-center mb-7">Sign in to continue</h2>
{{if .OIDCError}}
<div class="panel rounded-[7px] p-4 mb-5"
style="border-color: color-mix(in oklch, var(--bad), transparent 60%);">
<div class="text-bad text-[12.5px]">
{{if eq .OIDCError "no_role_match"}}Your account does not match any role mapping. Contact your administrator.
{{else if eq .OIDCError "username_taken"}}A local account with the same username already exists. Contact your administrator.
{{else if eq .OIDCError "user_disabled"}}Your account has been disabled. Contact your administrator.
{{else}}Sign-in via SSO failed ({{.OIDCError}}). Try again or use a local account.{{end}}
</div>
</div>
{{end}}
{{if .Error}} {{if .Error}}
<div class="mb-4 px-3 py-2.5 rounded-[5px] text-xs" <div class="mb-4 px-3 py-2.5 rounded-[5px] text-xs"
style="background: color-mix(in oklch, var(--bad), transparent 88%); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); color: oklch(0.85 0.10 25);"> style="background: color-mix(in oklch, var(--bad), transparent 88%); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); color: oklch(0.85 0.10 25);">
@@ -17,6 +29,17 @@
</div> </div>
{{end}} {{end}}
{{if .OIDCEnabled}}
<a href="/auth/oidc/login" class="btn btn-primary btn-block btn-lg mb-4">
Sign in with {{.OIDCDisplayName}}
</a>
<div class="flex items-center gap-3 my-5 text-[11px] text-ink-fade uppercase tracking-[0.08em]">
<div class="flex-1 border-t border-line-soft"></div>
<span>or sign in with a local account</span>
<div class="flex-1 border-t border-line-soft"></div>
</div>
{{end}}
<form method="post" action="/login"> <form method="post" action="/login">
<div class="mb-3.5"> <div class="mb-3.5">
<label class="field-label" for="login-username">Username</label> <label class="field-label" for="login-username">Username</label>
@@ -33,7 +56,7 @@
<p class="text-pretty text-xs text-ink-mute leading-[1.65]"> <p class="text-pretty text-xs text-ink-mute leading-[1.65]">
Forgot your password? An admin can reset it from Forgot your password? An admin can reset it from
<span class="mono text-ink-mid">Settings → Users</span>. <span class="mono text-ink-mid">Settings → Users</span>.
Theres no recovery email — this is self-hosted infrastructure. There's no recovery email — this is self-hosted infrastructure.
</p> </p>
</div> </div>
</div> </div>