From 28cfbadab11ffe12cbe9822536dd96b687fb72ca Mon Sep 17 00:00:00 2001 From: hopleus Date: Thu, 31 Oct 2024 12:43:49 +0300 Subject: [PATCH] Rewrite oidc_callback_template in elem-go --- hscontrol/assets/oidc_callback_template.html | 307 ------------------- hscontrol/oidc.go | 32 +- hscontrol/templates/general.go | 93 ++++++ hscontrol/templates/oidc_callback.go | 272 ++++++++++++++++ 4 files changed, 371 insertions(+), 333 deletions(-) delete mode 100644 hscontrol/assets/oidc_callback_template.html create mode 100644 hscontrol/templates/oidc_callback.go diff --git a/hscontrol/assets/oidc_callback_template.html b/hscontrol/assets/oidc_callback_template.html deleted file mode 100644 index 2236f365..00000000 --- a/hscontrol/assets/oidc_callback_template.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - Headscale Authentication Succeeded - - - -
-
- -
- -
-
Signed in via your OIDC provider
-

- {{.Verb}} as {{.User}}, you can now close this window. -

-
-
-
-

Not sure how to get started?

-

- Check out beginner and advanced guides on, or read more in the - documentation. -

- - - - - - - View the headscale documentation - - - - - - - - View the tailscale documentation - -
-
- - diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index 1db1ec07..e8c9f7f6 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -1,14 +1,13 @@ package hscontrol import ( - "bytes" "context" "crypto/rand" _ "embed" "encoding/hex" "errors" "fmt" - "html/template" + "github.com/juanfont/headscale/hscontrol/templates" "net/http" "slices" "strings" @@ -181,13 +180,6 @@ type oidcCallbackTemplateConfig struct { Verb string } -//go:embed assets/oidc_callback_template.html -var oidcCallbackTemplateContent string - -var oidcCallbackTemplate = template.Must( - template.New("oidccallback").Parse(oidcCallbackTemplateContent), -) - // OIDCCallbackHandler handles the callback from the OIDC endpoint // Retrieves the nkey from the state cache and adds the node to the users email user // TODO: A confirmation page for new nodes should be added to avoid phishing vulnerabilities @@ -252,19 +244,11 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler( return } - // TODO(kradalby): replace with go-elem - var content bytes.Buffer - if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{ - User: user.DisplayNameOrUsername(), - Verb: "Reauthenticated", - }); err != nil { - http.Error(writer, fmt.Errorf("rendering OIDC callback template: %w", err).Error(), http.StatusInternalServerError) - return - } + content := templates.OidcCallback(node, user, "Reauthenticated") writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.WriteHeader(http.StatusOK) - _, err = writer.Write(content.Bytes()) + _, err = writer.Write([]byte(content)) if err != nil { util.LogErr(err, "Failed to write response") } @@ -279,15 +263,11 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler( return } - content, err := renderOIDCCallbackTemplate(user) - if err != nil { - http.Error(writer, err.Error(), http.StatusInternalServerError) - return - } + content := templates.OidcCallback(node, user, "Authenticated") writer.Header().Set("Content-Type", "text/html; charset=utf-8") writer.WriteHeader(http.StatusOK) - if _, err := writer.Write(content.Bytes()); err != nil { + if _, err := writer.Write([]byte(content)); err != nil { util.LogErr(err, "Failed to write response") } @@ -519,7 +499,7 @@ func (a *AuthProviderOIDC) registerNode( } // TODO(kradalby): -// Rewrite in elem-go. +// Rewrite in elem-go func renderOIDCCallbackTemplate( user *types.User, ) (*bytes.Buffer, error) { diff --git a/hscontrol/templates/general.go b/hscontrol/templates/general.go index 3728b736..566ace46 100644 --- a/hscontrol/templates/general.go +++ b/hscontrol/templates/general.go @@ -20,6 +20,99 @@ var headerStyle = styles.Props{ styles.LineHeight: "1.2", } +func logo(class string) *elem.Element { + return &elem.Element{ + Tag: "svg", + Attrs: attrs.Props{ + attrs.Class: class, + attrs.Width: "146", + attrs.Height: "51", + "xmlns": "http://www.w3.org/2000/svg", + "xml:space": "preserve", + attrs.Style: styles.Props{ + styles.Var("fill-rule"): "evenodd", + styles.Var("clip-rule"): "evenodd", + styles.Var("stroke-linejoin"): "round", + styles.Var("stroke-miterlimit"): styles.Int(2), + }.ToInline(), + "viewBox": "0 0 1280 640", + }, + Children: []elem.Node{ + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + }, + } +} + +func iconSuccess(class string) *elem.Element { + return &elem.Element{ + Tag: "svg", + Attrs: attrs.Props{ + attrs.Class: class, + attrs.Width: "20", + attrs.Height: "20", + "viewBox": "0 0 512 512", + "xmlns": "http://www.w3.org/2000/svg", + }, + Children: []elem.Node{ + elem.Raw(""), + }, + } +} + +func iconWarning() *elem.Element { + return &elem.Element{ + Tag: "svg", + Attrs: attrs.Props{ + attrs.Width: "20", + attrs.Height: "20", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg", + attrs.Style: styles.Props{ + styles.MarginBottom: "1rem", + }.ToInline(), + }, + Children: []elem.Node{ + elem.Raw(""), + elem.Raw(""), + elem.Raw(""), + }, + } +} + +func iconExternalLink() *elem.Element { + return &elem.Element{ + Tag: "svg", + Attrs: attrs.Props{ + attrs.Width: "16", + attrs.Height: "16", + "viewBox": "0 0 16 16", + "fill": "currentColor", + "xmlns": "http://www.w3.org/2000/svg", + }, + Children: []elem.Node{ + elem.Raw(""), + }, + } +} + func headerOne(text string) *elem.Element { return elem.H1(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) } diff --git a/hscontrol/templates/oidc_callback.go b/hscontrol/templates/oidc_callback.go new file mode 100644 index 00000000..3b378d99 --- /dev/null +++ b/hscontrol/templates/oidc_callback.go @@ -0,0 +1,272 @@ +package templates + +import ( + "fmt" + "github.com/chasefleming/elem-go" + "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" + "github.com/juanfont/headscale/hscontrol/types" +) + +func OidcCallback(node *types.Node, user *types.User, verb string) string { + styleMgr := styles.NewStyleManager() + styleBody := styleMgr.AddStyle(styles.Props{ + styles.FontSize: styles.Pixels(14), + styles.FontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif", + }) + styleHr := styleMgr.AddStyle(styles.Props{ + styles.BorderColor: "#fdfdfe", + styles.Margin: "24px 0", + }) + classContainer := styleMgr.AddStyle(styles.Props{ + styles.Display: "flex", + styles.JustifyContent: "center", + styles.AlignItems: "center", + styles.Height: styles.ViewportHeight(70), + }) + classLogo := styleMgr.AddStyle(styles.Props{ + styles.Display: "block", + styles.MarginLeft: styles.Pixels(-20), + styles.MarginBottom: styles.Pixels(16), + }) + + colorMessageSuccess := styles.Props{ + styles.Background: "#fafdfa", + styles.Border: "1px solid #c6e9c9", + } + colorMessageWarning := styles.Props{ + styles.Background: "#fff9f2", + styles.Border: "1px solid #f7d7b0", + } + styleMessage := styles.Props{ + styles.Display: "flex", + styles.MinWidth: styles.ViewportWidth(40), + styles.MarginBottom: styles.Pixels(12), + styles.Padding: "12px 16px 16px 12px", + styles.Position: "relative", + styles.BorderRadius: styles.Pixels(2), + styles.FontSize: styles.Pixels(14), + } + + var classMessage string + if node.Approved { + classMessage = styleMgr.AddStyle(styles.Merge(styleMessage, colorMessageSuccess)) + } else { + classMessage = styleMgr.AddStyle(styles.Merge(styleMessage, colorMessageWarning)) + } + + classMessageContent := styleMgr.AddStyle(styles.Props{ + styles.MarginLeft: styles.Pixels(4), + }) + classIconSuccess := styleMgr.AddStyle(styles.Props{ + "fill": "#2eb039", + }) + + colorMessageTitleSuccess := styles.Props{ + styles.Color: "#1e7125", + } + colorMessageTitleWarning := styles.Props{ + styles.Color: "#d58525", + } + styleMessageTitle := styles.Props{ + styles.FontSize: styles.Pixels(16), + styles.FontWeight: styles.Int(700), + styles.LineHeight: styles.Float(1.25), + } + + var classMessageTitle string + if node.Approved { + classMessageTitle = styleMgr.AddStyle(styles.Merge(styleMessageTitle, colorMessageTitleSuccess)) + } else { + classMessageTitle = styleMgr.AddStyle(styles.Merge(styleMessageTitle, colorMessageTitleWarning)) + } + + colorMessageBodySuccess := styles.Props{ + styles.Color: "#17421b", + } + colorMessageBodyWarning := styles.Props{ + styles.Color: "#824c0b", + } + styleMessageBody := styles.Props{ + styles.FontSize: styles.Pixels(12), + styles.Margin: styles.Int(0), + styles.Padding: styles.Int(0), + styles.Border: styles.Int(0), + styles.MarginTop: styles.Pixels(4), + } + + var classMessageBody string + if node.Approved { + classMessageBody = styleMgr.AddStyle(styles.Merge(styleMessageBody, colorMessageBodySuccess)) + } else { + classMessageBody = styleMgr.AddStyle(styles.Merge(styleMessageBody, colorMessageBodyWarning)) + } + + styleA := styleMgr.AddCompositeStyle(styles.CompositeStyle{ + Default: styles.Props{ + styles.Display: "block", + styles.Margin: "8px 0", + styles.Color: "#1563ff", + styles.TextDecoration: "none", + styles.FontWeight: styles.Int(600), + }, + PseudoClasses: map[string]styles.Props{ + styles.PseudoHover: {styles.Color: "black"}, + }, + }) + classIcon := styleMgr.AddStyle(styles.Props{ + styles.AlignItems: "center", + styles.Display: "inline-flex", + styles.JustifyContent: "center", + styles.Width: styles.Pixels(21), + styles.Height: styles.Pixels(21), + styles.VerticalAlign: "middle", + }) + styleH1 := styleMgr.AddStyle(styles.Props{ + styles.FontSize: "17.5px", + styles.FontWeight: styles.Pixels(700), + styles.MarginBottom: styles.Pixels(0), + }) + styleH1P := styleMgr.AddStyle(styles.Props{ + styles.Margin: "8px 0 16px 0", + }) + + var messageText string + var icon *elem.Element + + if node.Approved { + messageText = fmt.Sprintf( + "%s as %s, you can now close this window.", + verb, + user.DisplayNameOrUsername(), + ) + icon = iconSuccess(classIconSuccess) + } else { + messageText = fmt.Sprintf( + "%s as %s, but not connected!", + verb, + user.DisplayNameOrUsername(), + ) + icon = iconWarning() + } + + description := &elem.Element{ + Tag: "span", + } + + if !node.Approved { + description = elem.Div( + nil, + elem.P( + attrs.Props{ + attrs.Class: classMessageBody, + attrs.Style: styles.Props{ + styles.MarginTop: "1rem", + }.ToInline(), + }, + elem.Text("However, it can't connect until approved by the administrator a network."), + ), + elem.P( + attrs.Props{ + attrs.Class: classMessageBody, + }, + elem.Text("Once approved, your node will connect automatically."), + ), + ) + } + + return HtmlStructure( + elem.Title( + nil, + elem.Text("headscale - Authentication Succeeded"), + ), + elem.Body( + attrs.Props{ + attrs.DataAttr("translate"): "no", + attrs.Class: styleBody, + }, + elem.Div( + attrs.Props{ + attrs.Class: classContainer, + }, + elem.Div( + nil, + logo(classLogo), + elem.Div( + attrs.Props{ + attrs.Class: classMessage, + }, + icon, + elem.Div( + attrs.Props{ + attrs.Class: classMessageContent, + }, + elem.Div( + attrs.Props{ + attrs.Class: classMessageTitle, + }, + elem.Text("Signed in via your OIDC provider"), + ), + elem.P( + attrs.Props{ + attrs.Class: classMessageBody, + }, + elem.Text(messageText), + ), + description, + ), + ), + elem.Hr( + attrs.Props{ + attrs.Class: styleHr, + }, + ), + elem.H1( + attrs.Props{ + attrs.Class: styleH1, + }, + elem.Text("Not sure how to get started?"), + ), + elem.P( + attrs.Props{ + attrs.Class: styleH1P, + }, + elem.Text(" Check out beginner and advanced guides on, or read more in the documentation."), + ), + elem.A( + attrs.Props{ + attrs.Href: "https://github.com/juanfont/headscale/tree/main/docs", + attrs.Rel: "noreferrer noopener", + attrs.Target: "_blank", + attrs.Class: styleA, + }, + elem.Span( + attrs.Props{ + attrs.Class: classIcon, + }, + iconExternalLink(), + ), + elem.Text("View the headscale documentation"), + ), + elem.A( + attrs.Props{ + attrs.Href: "https://tailscale.com/kb/", + attrs.Rel: "noreferrer noopener", + attrs.Target: "_blank", + attrs.Class: styleA, + }, + elem.Span( + attrs.Props{ + attrs.Class: classIcon, + }, + iconExternalLink(), + ), + elem.Text("View the tailscale documentation"), + ), + ), + ), + ), + ).RenderWithOptions(elem.RenderOptions{ + StyleManager: styleMgr, + }) +}