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:
parent
cb3b6949ea
commit
4a7e1475c0
@ -262,6 +262,23 @@ func (a *AuthProviderWeb) AuthHandler(
|
|||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
) {
|
) {
|
||||||
|
authID, err := authIDFromRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
httpError(writer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
_, err = writer.Write([]byte(templates.AuthWeb(
|
||||||
|
"Authentication check",
|
||||||
|
"Run the command below in the headscale server to approve this authentication request:",
|
||||||
|
"headscale auth approve --auth-id "+authID.String(),
|
||||||
|
).Render()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to write auth response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func authIDFromRequest(req *http.Request) (types.AuthID, error) {
|
func authIDFromRequest(req *http.Request) (types.AuthID, error) {
|
||||||
@ -299,7 +316,11 @@ func (a *AuthProviderWeb) RegisterHandler(
|
|||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
_, err = writer.Write([]byte(templates.RegisterWeb(registrationId).Render()))
|
_, err = writer.Write([]byte(templates.AuthWeb(
|
||||||
|
"Node registration",
|
||||||
|
"Run the command below in the headscale server to add this node to your network:",
|
||||||
|
fmt.Sprintf("headscale auth register --auth-id %s --user USERNAME", registrationId.String()),
|
||||||
|
).Render()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to write register response")
|
log.Error().Err(err).Msg("failed to write register response")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -333,8 +333,6 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
|||||||
|
|
||||||
// If this is a registration flow, then we need to register the node.
|
// If this is a registration flow, then we need to register the node.
|
||||||
if authInfo.Registration {
|
if authInfo.Registration {
|
||||||
verb := "Reauthenticated"
|
|
||||||
|
|
||||||
newNode, err := a.handleRegistration(user, authInfo.AuthID, nodeExpiry)
|
newNode, err := a.handleRegistration(user, authInfo.AuthID, nodeExpiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNodeNotFoundRegistrationCache) {
|
if errors.Is(err, db.ErrNodeNotFoundRegistrationCache) {
|
||||||
@ -349,12 +347,7 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if newNode {
|
content := renderRegistrationSuccessTemplate(user, newNode)
|
||||||
verb = "Authenticated"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kradalby): replace with go-elem
|
|
||||||
content := renderOIDCCallbackTemplate(user, verb)
|
|
||||||
|
|
||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
@ -366,8 +359,28 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): handle login flow (without registration) if needed.
|
// If this is not a registration callback, then its a regular authentication callback
|
||||||
// We need to send an update here to whatever might be waiting for this auth flow.
|
// and we need to send a response and confirm that the access was allowed.
|
||||||
|
|
||||||
|
authReq, ok := a.h.state.GetAuthCacheEntry(authInfo.AuthID)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Caller().Str("auth_id", authInfo.AuthID.String()).Msg("auth session expired before authorization completed")
|
||||||
|
httpError(writer, NewHTTPError(http.StatusGone, "login session expired, try again", nil))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a finish auth verdict with no errors to let the CLI know that the authentication was successful.
|
||||||
|
authReq.FinishAuth(types.AuthVerdict{})
|
||||||
|
|
||||||
|
content := renderAuthSuccessTemplate(user)
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
if _, err := writer.Write(content.Bytes()); err != nil { //nolint:noinlineerr
|
||||||
|
util.LogErr(err, "Failed to write HTTP response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time.Time {
|
func (a *AuthProviderOIDC) determineNodeExpiry(idTokenExpiration time.Time) time.Time {
|
||||||
@ -623,12 +636,38 @@ func (a *AuthProviderOIDC) handleRegistration(
|
|||||||
return !nodeChange.IsEmpty(), nil
|
return !nodeChange.IsEmpty(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderOIDCCallbackTemplate(
|
func renderRegistrationSuccessTemplate(
|
||||||
user *types.User,
|
user *types.User,
|
||||||
verb string,
|
newNode bool,
|
||||||
) *bytes.Buffer {
|
) *bytes.Buffer {
|
||||||
html := templates.OIDCCallback(user.Display(), verb).Render()
|
result := templates.AuthSuccessResult{
|
||||||
return bytes.NewBufferString(html)
|
Title: "Headscale - Node Reauthenticated",
|
||||||
|
Heading: "Node reauthenticated",
|
||||||
|
Verb: "Reauthenticated",
|
||||||
|
User: user.Display(),
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
}
|
||||||
|
if newNode {
|
||||||
|
result.Title = "Headscale - Node Registered"
|
||||||
|
result.Heading = "Node registered"
|
||||||
|
result.Verb = "Registered"
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewBufferString(templates.AuthSuccess(result).Render())
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAuthSuccessTemplate(
|
||||||
|
user *types.User,
|
||||||
|
) *bytes.Buffer {
|
||||||
|
result := templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - SSH Session Authorized",
|
||||||
|
Heading: "SSH session authorized",
|
||||||
|
Verb: "Authorized",
|
||||||
|
User: user.Display(),
|
||||||
|
Message: "You may return to your terminal.",
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewBufferString(templates.AuthSuccess(result).Render())
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCookieName generates a unique cookie name based on a cookie value.
|
// getCookieName generates a unique cookie name based on a cookie value.
|
||||||
|
|||||||
@ -7,35 +7,54 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOIDCCallbackTemplate(t *testing.T) {
|
func TestAuthSuccessTemplate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
userName string
|
result templates.AuthSuccessResult
|
||||||
verb string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "logged_in_user",
|
name: "node_registered",
|
||||||
userName: "test@example.com",
|
result: templates.AuthSuccessResult{
|
||||||
verb: "Logged in",
|
Title: "Headscale - Node Registered",
|
||||||
|
Heading: "Node registered",
|
||||||
|
Verb: "Registered",
|
||||||
|
User: "newuser@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "registered_user",
|
name: "node_reauthenticated",
|
||||||
userName: "newuser@example.com",
|
result: templates.AuthSuccessResult{
|
||||||
verb: "Registered",
|
Title: "Headscale - Node Reauthenticated",
|
||||||
|
Heading: "Node reauthenticated",
|
||||||
|
Verb: "Reauthenticated",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh_session_authorized",
|
||||||
|
result: templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - SSH Session Authorized",
|
||||||
|
Heading: "SSH session authorized",
|
||||||
|
Verb: "Authorized",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You may return to your terminal.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Render using the elem-go template
|
html := templates.AuthSuccess(tt.result).Render()
|
||||||
html := templates.OIDCCallback(tt.userName, tt.verb).Render()
|
|
||||||
|
|
||||||
// Verify the HTML contains expected elements
|
// Verify the HTML contains expected structural elements
|
||||||
assert.Contains(t, html, "<!DOCTYPE html>")
|
assert.Contains(t, html, "<!DOCTYPE html>")
|
||||||
assert.Contains(t, html, "<title>Headscale Authentication Succeeded</title>")
|
assert.Contains(t, html, "<title>"+tt.result.Title+"</title>")
|
||||||
assert.Contains(t, html, tt.verb)
|
assert.Contains(t, html, tt.result.Heading)
|
||||||
assert.Contains(t, html, tt.userName)
|
assert.Contains(t, html, tt.result.Verb+" as ")
|
||||||
assert.Contains(t, html, "You can now close this window")
|
assert.Contains(t, html, tt.result.User)
|
||||||
|
assert.Contains(t, html, tt.result.Message)
|
||||||
|
|
||||||
// Verify Material for MkDocs design system CSS is present
|
// Verify Material for MkDocs design system CSS is present
|
||||||
assert.Contains(t, html, "Material for MkDocs")
|
assert.Contains(t, html, "Material for MkDocs")
|
||||||
|
|||||||
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.
|
// warningBox creates a warning message box with icon and content.
|
||||||
//
|
//
|
||||||
//nolint:unused // Used in apple.go template.
|
//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(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/templates"
|
"github.com/juanfont/headscale/hscontrol/templates"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,12 +15,30 @@ func TestTemplateHTMLConsistency(t *testing.T) {
|
|||||||
html string
|
html string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OIDC Callback",
|
name: "Auth Success",
|
||||||
html: templates.OIDCCallback("test@example.com", "Logged in").Render(),
|
html: templates.AuthSuccess(templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - Node Registered",
|
||||||
|
Heading: "Node registered",
|
||||||
|
Verb: "Registered",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
}).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Register Web",
|
name: "Auth Web Register",
|
||||||
html: templates.RegisterWeb(types.AuthID("test-key-123")).Render(),
|
html: templates.AuthWeb(
|
||||||
|
"Machine registration",
|
||||||
|
"Run the command below in the headscale server to add this machine to your network:",
|
||||||
|
"headscale auth register --auth-id test-key-123 --user USERNAME",
|
||||||
|
).Render(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth Web Approve",
|
||||||
|
html: templates.AuthWeb(
|
||||||
|
"Authentication check",
|
||||||
|
"Run the command below in the headscale server to approve this authentication request:",
|
||||||
|
"headscale auth approve --auth-id test-key-123",
|
||||||
|
).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Windows Config",
|
name: "Windows Config",
|
||||||
@ -72,12 +89,30 @@ func TestTemplateModernHTMLFeatures(t *testing.T) {
|
|||||||
html string
|
html string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OIDC Callback",
|
name: "Auth Success",
|
||||||
html: templates.OIDCCallback("test@example.com", "Logged in").Render(),
|
html: templates.AuthSuccess(templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - Node Registered",
|
||||||
|
Heading: "Node registered",
|
||||||
|
Verb: "Registered",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
}).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Register Web",
|
name: "Auth Web Register",
|
||||||
html: templates.RegisterWeb(types.AuthID("test-key-123")).Render(),
|
html: templates.AuthWeb(
|
||||||
|
"Machine registration",
|
||||||
|
"Run the command below in the headscale server to add this machine to your network:",
|
||||||
|
"headscale auth register --auth-id test-key-123 --user USERNAME",
|
||||||
|
).Render(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth Web Approve",
|
||||||
|
html: templates.AuthWeb(
|
||||||
|
"Authentication check",
|
||||||
|
"Run the command below in the headscale server to approve this authentication request:",
|
||||||
|
"headscale auth approve --auth-id test-key-123",
|
||||||
|
).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Windows Config",
|
name: "Windows Config",
|
||||||
@ -116,16 +151,35 @@ func TestTemplateExternalLinkSecurity(t *testing.T) {
|
|||||||
externalURLs []string // URLs that should have security attributes
|
externalURLs []string // URLs that should have security attributes
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OIDC Callback",
|
name: "Auth Success",
|
||||||
html: templates.OIDCCallback("test@example.com", "Logged in").Render(),
|
html: templates.AuthSuccess(templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - Node Registered",
|
||||||
|
Heading: "Node registered",
|
||||||
|
Verb: "Registered",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
}).Render(),
|
||||||
externalURLs: []string{
|
externalURLs: []string{
|
||||||
"https://headscale.net/stable/",
|
"https://headscale.net/stable/",
|
||||||
"https://tailscale.com/kb/",
|
"https://tailscale.com/kb/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Register Web",
|
name: "Auth Web Register",
|
||||||
html: templates.RegisterWeb(types.AuthID("test-key-123")).Render(),
|
html: templates.AuthWeb(
|
||||||
|
"Machine registration",
|
||||||
|
"Run the command below in the headscale server to add this machine to your network:",
|
||||||
|
"headscale auth register --auth-id test-key-123 --user USERNAME",
|
||||||
|
).Render(),
|
||||||
|
externalURLs: []string{}, // No external links
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth Web Approve",
|
||||||
|
html: templates.AuthWeb(
|
||||||
|
"Authentication check",
|
||||||
|
"Run the command below in the headscale server to approve this authentication request:",
|
||||||
|
"headscale auth approve --auth-id test-key-123",
|
||||||
|
).Render(),
|
||||||
externalURLs: []string{}, // No external links
|
externalURLs: []string{}, // No external links
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -185,12 +239,30 @@ func TestTemplateAccessibilityAttributes(t *testing.T) {
|
|||||||
html string
|
html string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OIDC Callback",
|
name: "Auth Success",
|
||||||
html: templates.OIDCCallback("test@example.com", "Logged in").Render(),
|
html: templates.AuthSuccess(templates.AuthSuccessResult{
|
||||||
|
Title: "Headscale - Node Registered",
|
||||||
|
Heading: "Node registered",
|
||||||
|
Verb: "Registered",
|
||||||
|
User: "test@example.com",
|
||||||
|
Message: "You can now close this window.",
|
||||||
|
}).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Register Web",
|
name: "Auth Web Register",
|
||||||
html: templates.RegisterWeb(types.AuthID("test-key-123")).Render(),
|
html: templates.AuthWeb(
|
||||||
|
"Machine registration",
|
||||||
|
"Run the command below in the headscale server to add this machine to your network:",
|
||||||
|
"headscale auth register --auth-id test-key-123 --user USERNAME",
|
||||||
|
).Render(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth Web Approve",
|
||||||
|
html: templates.AuthWeb(
|
||||||
|
"Authentication check",
|
||||||
|
"Run the command below in the headscale server to approve this authentication request:",
|
||||||
|
"headscale auth approve --auth-id test-key-123",
|
||||||
|
).Render(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Windows Config",
|
name: "Windows Config",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user