mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-25 01:59:07 +02:00
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:
62
hscontrol/templates/auth_success.go
Normal file
62
hscontrol/templates/auth_success.go
Normal 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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
21
hscontrol/templates/auth_web.go
Normal file
21
hscontrol/templates/auth_web.go
Normal 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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user