feat(auth): custom logout page with Authelia sign-out and re-login button

This commit is contained in:
Claus Lohmar 2026-06-15 08:24:13 +00:00
parent 594cf3efe2
commit 312aa388d4
3 changed files with 109 additions and 12 deletions

40
src/core/ui/logout.templ Normal file
View file

@ -0,0 +1,40 @@
package ui
templ LogoutPage(authLogoutURL, workspaceURL string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Logged Out — Next Workspace</title>
<style type="text/css">
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;min-height:100vh}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:40px;text-align:center;max-width:380px;width:90%}
h1{font-size:1.4rem;margin-bottom:8px}
p{color:#8b949e;font-size:0.9rem;margin-bottom:24px;line-height:1.5}
.btn{display:block;width:100%;padding:12px;border-radius:8px;border:none;font-size:0.9rem;font-weight:600;cursor:pointer;text-decoration:none;margin-bottom:10px;text-align:center}
.btn-primary{background:#58a6ff;color:#fff}
.btn-primary:hover{opacity:0.9}
.btn-secondary{background:transparent;border:1px solid #30363d;color:#8b949e}
.btn-secondary:hover{background:#21262d}
.status{color:#3fb950;font-size:0.85rem;margin-bottom:16px}
</style>
</head>
<body>
<div class="card">
<h1>Logged Out</h1>
<div class="status">✓ Session cleared</div>
<p>You have been logged out of Next Workspace. Your identity provider session will also be cleared.</p>
<a href={ workspaceURL } class="btn btn-primary">Re-login</a>
<a href={ authLogoutURL } class="btn btn-secondary">Sign out from Authelia</a>
</div>
<script type="text/javascript">
// Redirect to Authelia logout after a short delay
setTimeout(function() {
window.location.href = document.querySelector('.btn-secondary').href;
}, 1500);
</script>
</body>
</html>
}

View file

@ -0,0 +1,66 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func LogoutPage(authLogoutURL, workspaceURL string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Logged Out — Next Workspace</title><style type=\"text/css\">\n\t\t\t\t*{margin:0;padding:0;box-sizing:border-box}\n\t\t\t\tbody{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;min-height:100vh}\n\t\t\t\t.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:40px;text-align:center;max-width:380px;width:90%}\n\t\t\t\th1{font-size:1.4rem;margin-bottom:8px}\n\t\t\t\tp{color:#8b949e;font-size:0.9rem;margin-bottom:24px;line-height:1.5}\n\t\t\t\t.btn{display:block;width:100%;padding:12px;border-radius:8px;border:none;font-size:0.9rem;font-weight:600;cursor:pointer;text-decoration:none;margin-bottom:10px;text-align:center}\n\t\t\t\t.btn-primary{background:#58a6ff;color:#fff}\n\t\t\t\t.btn-primary:hover{opacity:0.9}\n\t\t\t\t.btn-secondary{background:transparent;border:1px solid #30363d;color:#8b949e}\n\t\t\t\t.btn-secondary:hover{background:#21262d}\n\t\t\t\t.status{color:#3fb950;font-size:0.85rem;margin-bottom:16px}\n\t\t\t</style></head><body><div class=\"card\"><h1>Logged Out</h1><div class=\"status\">✓ Session cleared</div><p>You have been logged out of Next Workspace. Your identity provider session will also be cleared.</p><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(workspaceURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/ui/logout.templ`, Line: 29, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"btn btn-primary\">Re-login</a> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(authLogoutURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/ui/logout.templ`, Line: 30, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"btn btn-secondary\">Sign out from Authelia</a></div><script type=\"text/javascript\">\n\t\t\t\t// Redirect to Authelia logout after a short delay\n\t\t\t\tsetTimeout(function() {\n\t\t\t\t\twindow.location.href = document.querySelector('.btn-secondary').href;\n\t\t\t\t}, 1500);\n\t\t\t</script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View file

@ -123,23 +123,14 @@ func main() {
mux.HandleFunc("GET /auth/callback", oidcHandler.Callback)
mux.HandleFunc("POST /auth/callback", oidcHandler.Callback)
mux.HandleFunc("GET /auth/logout", func(w http.ResponseWriter, r *http.Request) {
// Clear session cookie (with domain to match)
http.SetCookie(w, &http.Cookie{
Name: "nextwks_session",
Value: "",
Path: "/",
Domain: cfg.OIDC.Domain,
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
// Clear NextWks session cookie
http.SetCookie(w, &http.Cookie{
Name: "nextwks_session", Value: "", Path: "/", Domain: cfg.OIDC.Domain,
MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode,
})
// Redirect to workspace — no session means redirect to Authelia login
http.Redirect(w, r, "/", http.StatusFound)
authLogoutURL := fmt.Sprintf("%s/logout", cfg.OIDC.IssuerURL)
component := ui.LogoutPage(authLogoutURL, cfg.OIDC.RedirectURL)
component.Render(r.Context(), w)
})
// --- Workspace launcher (public, but OIDC-protected) ---