templates: generalise auth templates for web and OIDC

Extract shared HTML/CSS design into a common template and create
generalised auth success and web auth templates that work for both
node registration and SSH check authentication flows.

Updates #1850
This commit is contained in:
Kristoffer Dalby
2026-02-24 18:49:18 +00:00
parent cb3b6949ea
commit 4a7e1475c0
9 changed files with 324 additions and 139 deletions

View File

@@ -0,0 +1,62 @@
package templates
import (
"github.com/chasefleming/elem-go"
)
// AuthSuccessResult contains the text content for an authentication success page.
// Each field controls a distinct piece of user-facing text so that every auth
// flow (node registration, reauthentication, SSH check, …) can clearly
// communicate what just happened.
type AuthSuccessResult struct {
// Title is the browser tab / page title,
// e.g. "Headscale - Node Registered".
Title string
// Heading is the bold green text inside the success box,
// e.g. "Node registered".
Heading string
// Verb is the action prefix in the body text before "as <user>",
// e.g. "Registered", "Reauthenticated", "Authorized".
Verb string
// User is the display name shown in bold in the body text,
// e.g. "user@example.com".
User string
// Message is the follow-up instruction shown after the user name,
// e.g. "You can now close this window."
Message string
}
// AuthSuccess renders an authentication / authorisation success page.
// The caller controls every user-visible string via [AuthSuccessResult] so the
// page clearly describes what succeeded (registration, reauth, SSH check, …).
func AuthSuccess(result AuthSuccessResult) *elem.Element {
box := successBox(
result.Heading,
elem.Text(result.Verb+" as "),
elem.Strong(nil, elem.Text(result.User)),
elem.Text(". "+result.Message),
)
return HtmlStructure(
elem.Title(nil, elem.Text(result.Title)),
mdTypesetBody(
headscaleLogo(),
box,
H2(elem.Text("Getting started")),
P(elem.Text("Check out the documentation to learn more about headscale and Tailscale:")),
Ul(
elem.Li(nil,
externalLink("https://headscale.net/stable/", "Headscale documentation"),
),
elem.Li(nil,
externalLink("https://tailscale.com/kb/", "Tailscale knowledge base"),
),
),
pageFooter(),
),
)
}

View File

@@ -0,0 +1,21 @@
package templates
import (
"github.com/chasefleming/elem-go"
)
// AuthWeb renders a page that instructs an administrator to run a CLI command
// to complete an authentication or registration flow.
// It is used by both the registration and auth-approve web handlers.
func AuthWeb(title, description, command string) *elem.Element {
return HtmlStructure(
elem.Title(nil, elem.Text(title+" - Headscale")),
mdTypesetBody(
headscaleLogo(),
H1(elem.Text(title)),
P(elem.Text(description)),
Pre(PreCode(command)),
pageFooter(),
),
)
}

View File

@@ -365,6 +365,47 @@ func orDivider() *elem.Element {
)
}
// successBox creates a green success feedback box with a checkmark icon.
// The heading is displayed as bold green text, and children are rendered below it.
// Pairs with warningBox for consistent feedback styling.
//
//nolint:unused // Used in auth_success.go template.
func successBox(heading string, children ...elem.Node) *elem.Element {
return elem.Div(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "flex",
styles.AlignItems: "center",
styles.Gap: spaceM,
styles.Padding: spaceL,
styles.BackgroundColor: colorSuccessLight,
styles.Border: "1px solid " + colorSuccess,
styles.BorderRadius: "0.5rem",
styles.MarginBottom: spaceXL,
}.ToInline(),
},
checkboxIcon(),
elem.Div(nil,
append([]elem.Node{
elem.Strong(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "block",
styles.Color: colorSuccess,
styles.FontSize: fontSizeH3,
styles.MarginBottom: spaceXS,
}.ToInline(),
}, elem.Text(heading)),
}, children...)...,
),
)
}
// checkboxIcon returns the success checkbox SVG icon as raw HTML.
func checkboxIcon() elem.Node {
return elem.Raw(`<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512">
<path d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm114.9 149.1L231.8 359.6c-1.1 1.1-2.9 3.5-5.1 3.5-2.3 0-3.8-1.6-5.1-2.9-1.3-1.3-78.9-75.9-78.9-75.9l-1.5-1.5c-.6-.9-1.1-2-1.1-3.2 0-1.2.5-2.3 1.1-3.2.4-.4.7-.7 1.1-1.2 7.7-8.1 23.3-24.5 24.3-25.5 1.3-1.3 2.4-3 4.8-3 2.5 0 4.1 2.1 5.3 3.3 1.2 1.2 45 43.3 45 43.3l111.3-143c1-.8 2.2-1.4 3.5-1.4 1.3 0 2.5.5 3.5 1.3l30.6 24.1c.8 1 1.3 2.2 1.3 3.5.1 1.3-.4 2.4-1 3.3z"></path>
</svg>`)
}
// warningBox creates a warning message box with icon and content.
//
//nolint:unused // Used in apple.go template.

View File

@@ -1,69 +0,0 @@
package templates
import (
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/chasefleming/elem-go/styles"
)
// checkboxIcon returns the success checkbox SVG icon as raw HTML.
func checkboxIcon() elem.Node {
return elem.Raw(`<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512">
<path d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm114.9 149.1L231.8 359.6c-1.1 1.1-2.9 3.5-5.1 3.5-2.3 0-3.8-1.6-5.1-2.9-1.3-1.3-78.9-75.9-78.9-75.9l-1.5-1.5c-.6-.9-1.1-2-1.1-3.2 0-1.2.5-2.3 1.1-3.2.4-.4.7-.7 1.1-1.2 7.7-8.1 23.3-24.5 24.3-25.5 1.3-1.3 2.4-3 4.8-3 2.5 0 4.1 2.1 5.3 3.3 1.2 1.2 45 43.3 45 43.3l111.3-143c1-.8 2.2-1.4 3.5-1.4 1.3 0 2.5.5 3.5 1.3l30.6 24.1c.8 1 1.3 2.2 1.3 3.5.1 1.3-.4 2.4-1 3.3z"></path>
</svg>`)
}
// OIDCCallback renders the OIDC authentication success callback page.
func OIDCCallback(user, verb string) *elem.Element {
// Success message box
successBox := elem.Div(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "flex",
styles.AlignItems: "center",
styles.Gap: spaceM,
styles.Padding: spaceL,
styles.BackgroundColor: colorSuccessLight,
styles.Border: "1px solid " + colorSuccess,
styles.BorderRadius: "0.5rem",
styles.MarginBottom: spaceXL,
}.ToInline(),
},
checkboxIcon(),
elem.Div(nil,
elem.Strong(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "block",
styles.Color: colorSuccess,
styles.FontSize: fontSizeH3,
styles.MarginBottom: spaceXS,
}.ToInline(),
}, elem.Text("Signed in successfully")),
elem.P(attrs.Props{
attrs.Style: styles.Props{
styles.Margin: "0",
styles.Color: colorTextPrimary,
styles.FontSize: fontSizeBase,
}.ToInline(),
}, elem.Text(verb), elem.Text(" as "), elem.Strong(nil, elem.Text(user)), elem.Text(". You can now close this window.")),
),
)
return HtmlStructure(
elem.Title(nil, elem.Text("Headscale Authentication Succeeded")),
mdTypesetBody(
headscaleLogo(),
successBox,
H2(elem.Text("Getting started")),
P(elem.Text("Check out the documentation to learn more about headscale and Tailscale:")),
Ul(
elem.Li(nil,
externalLink("https://headscale.net/stable/", "Headscale documentation"),
),
elem.Li(nil,
externalLink("https://tailscale.com/kb/", "Tailscale knowledge base"),
),
),
pageFooter(),
),
)
}

View File

@@ -1,21 +0,0 @@
package templates
import (
"fmt"
"github.com/chasefleming/elem-go"
"github.com/juanfont/headscale/hscontrol/types"
)
func RegisterWeb(registrationID types.AuthID) *elem.Element {
return HtmlStructure(
elem.Title(nil, elem.Text("Registration - Headscale")),
mdTypesetBody(
headscaleLogo(),
H1(elem.Text("Machine registration")),
P(elem.Text("Run the command below in the headscale server to add this machine to your network:")),
Pre(PreCode(fmt.Sprintf("headscale nodes register --key %s --user USERNAME", registrationID.String()))),
pageFooter(),
),
)
}