Can't log in from a fresh LXC deploy, and the service is LAN-only by
design. Rip out the whole bcrypt-password / signed-cookie session
layer: internal/auth, login templates, gen-admin-password binary +
Makefile targets, auth config block, login/logout routes and the
RequireSession middleware wrap. Agent bearer-token auth on
/api/v1/runs/{id}/* is untouched.
Operators who want a password can front the service with a reverse
proxy — noted in README and docs/operations.md.
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"vetting/internal/auth"
|
||||
"vetting/internal/events"
|
||||
"vetting/internal/model"
|
||||
"vetting/internal/orchestrator"
|
||||
@@ -23,7 +22,6 @@ type UI struct {
|
||||
Hosts *store.Hosts
|
||||
Runs *store.Runs
|
||||
Artifacts *store.Artifacts
|
||||
Auth *auth.Manager
|
||||
EventHub *events.Hub
|
||||
Runner *orchestrator.Runner
|
||||
Tiles *TileEnricher
|
||||
@@ -93,38 +91,6 @@ func (u *UI) StartRun(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (u *UI) LoginForm(w http.ResponseWriter, r *http.Request) {
|
||||
next := r.URL.Query().Get("next")
|
||||
if next == "" {
|
||||
next = "/"
|
||||
}
|
||||
_ = templates.Login("", next).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func (u *UI) LoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
password := r.PostForm.Get("password")
|
||||
next := r.PostForm.Get("next")
|
||||
if next == "" || !strings.HasPrefix(next, "/") {
|
||||
next = "/"
|
||||
}
|
||||
if !u.Auth.VerifyPassword(password) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = templates.Login("Invalid password.", next).Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
u.Auth.Issue(w, r)
|
||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (u *UI) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
u.Auth.Clear(w)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) {
|
||||
_ = templates.Registration(templates.RegistrationForm{}).Render(r.Context(), w)
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequireSession redirects unauthenticated requests to /login.
|
||||
func (m *Manager) RequireSession(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := m.Validate(r); err != nil {
|
||||
if acceptsHTML(r) {
|
||||
http.Redirect(w, r, "/login?next="+r.URL.RequestURI(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func acceptsHTML(r *http.Request) bool {
|
||||
accept := r.Header.Get("Accept")
|
||||
if accept == "" {
|
||||
return true
|
||||
}
|
||||
for _, part := range splitComma(accept) {
|
||||
if part == "text/html" || part == "*/*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
var out []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == ',' {
|
||||
out = append(out, trimSpace(s[start:i]))
|
||||
start = i + 1
|
||||
} else if s[i] == ';' {
|
||||
out = append(out, trimSpace(s[start:i]))
|
||||
for i < len(s) && s[i] != ',' {
|
||||
i++
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
out = append(out, trimSpace(s[start:]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func trimSpace(s string) string {
|
||||
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
|
||||
s = s[1:]
|
||||
}
|
||||
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const cookieName = "vetting_session"
|
||||
|
||||
type Manager struct {
|
||||
PasswordHash string
|
||||
Secret []byte
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
func (m *Manager) VerifyPassword(password string) bool {
|
||||
if m.PasswordHash == "" {
|
||||
return false
|
||||
}
|
||||
return bcrypt.CompareHashAndPassword([]byte(m.PasswordHash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// Issue writes a signed session cookie valid for m.TTL.
|
||||
func (m *Manager) Issue(w http.ResponseWriter, r *http.Request) {
|
||||
expiry := time.Now().Add(m.TTL).Unix()
|
||||
payload := strconv.FormatInt(expiry, 10)
|
||||
sig := m.sign(payload)
|
||||
value := payload + "." + sig
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Unix(expiry, 0),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Clear(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
var errInvalidSession = errors.New("invalid session")
|
||||
|
||||
// Validate returns nil if the request's cookie is present, signed, and not expired.
|
||||
func (m *Manager) Validate(r *http.Request) error {
|
||||
c, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return errInvalidSession
|
||||
}
|
||||
parts := strings.SplitN(c.Value, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return errInvalidSession
|
||||
}
|
||||
payload, sig := parts[0], parts[1]
|
||||
expected := m.sign(payload)
|
||||
if !hmac.Equal([]byte(sig), []byte(expected)) {
|
||||
return errInvalidSession
|
||||
}
|
||||
expiry, err := strconv.ParseInt(payload, 10, 64)
|
||||
if err != nil {
|
||||
return errInvalidSession
|
||||
}
|
||||
if time.Now().Unix() >= expiry {
|
||||
return errInvalidSession
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) sign(payload string) string {
|
||||
mac := hmac.New(sha256.New, m.Secret)
|
||||
_, _ = mac.Write([]byte(payload))
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// BcryptHash is a helper used by the gen-admin-password tool.
|
||||
func BcryptHash(password string) (string, error) {
|
||||
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bcrypt: %w", err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -13,7 +12,6 @@ type Config struct {
|
||||
Database Database `yaml:"database"`
|
||||
Artifacts Artifacts `yaml:"artifacts"`
|
||||
Logs Logs `yaml:"logs"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Dispatcher Dispatcher `yaml:"dispatcher"`
|
||||
Janitor Janitor `yaml:"janitor"`
|
||||
PXE PXE `yaml:"pxe"`
|
||||
@@ -52,23 +50,6 @@ type Janitor struct {
|
||||
IntervalMinutes int `yaml:"interval_minutes"` // 0 = 60
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
AdminPasswordBcrypt string `yaml:"admin_password_bcrypt"`
|
||||
SessionSecretHex string `yaml:"session_secret_hex"`
|
||||
SessionTTLHours int `yaml:"session_ttl_hours"`
|
||||
}
|
||||
|
||||
func (a Auth) SessionSecret() ([]byte, error) {
|
||||
b, err := hex.DecodeString(a.SessionSecretHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("session_secret_hex: %w", err)
|
||||
}
|
||||
if len(b) < 32 {
|
||||
return nil, fmt.Errorf("session_secret_hex must decode to at least 32 bytes, got %d", len(b))
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
type Dispatcher struct {
|
||||
MaxConcurrentRuns int `yaml:"max_concurrent_runs"`
|
||||
}
|
||||
@@ -132,9 +113,6 @@ func Load(path string) (*Config, error) {
|
||||
if c.Logs.Dir == "" {
|
||||
c.Logs.Dir = "./var/logs"
|
||||
}
|
||||
if c.Auth.SessionTTLHours == 0 {
|
||||
c.Auth.SessionTTLHours = 24
|
||||
}
|
||||
if c.Dispatcher.MaxConcurrentRuns == 0 {
|
||||
c.Dispatcher.MaxConcurrentRuns = 3
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ import (
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"vetting/internal/api"
|
||||
"vetting/internal/auth"
|
||||
"vetting/internal/web"
|
||||
)
|
||||
|
||||
type Deps struct {
|
||||
Auth *auth.Manager
|
||||
UI *api.UI
|
||||
Agent *api.Agent
|
||||
LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live
|
||||
@@ -38,13 +36,8 @@ func NewRouter(d Deps) http.Handler {
|
||||
r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir))))
|
||||
}
|
||||
|
||||
// Public (no session required) endpoints.
|
||||
r.Get("/login", d.UI.LoginForm)
|
||||
r.Post("/login", d.UI.LoginSubmit)
|
||||
r.Post("/logout", d.UI.Logout)
|
||||
|
||||
// Agent / PXE endpoints — authenticated per-request by bearer token
|
||||
// or by the unforgeable MAC path parameter, never by the UI session.
|
||||
// or by the unforgeable MAC path parameter.
|
||||
r.Get("/ipxe/{mac}", d.Agent.IPXEScript)
|
||||
r.Route("/api/v1/runs/{id}", func(r chi.Router) {
|
||||
r.Post("/hello", d.Agent.Hello)
|
||||
@@ -56,20 +49,16 @@ func NewRouter(d Deps) http.Handler {
|
||||
r.Post("/sensor", d.Agent.Sensor)
|
||||
})
|
||||
|
||||
// Session-gated browser UI.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(d.Auth.RequireSession)
|
||||
|
||||
r.Get("/", d.UI.Dashboard)
|
||||
r.Get("/hosts/new", d.UI.NewHostForm)
|
||||
r.Post("/hosts", d.UI.CreateHost)
|
||||
r.Post("/hosts/{id}/delete", d.UI.DeleteHost)
|
||||
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
||||
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
||||
r.Get("/reports/{runID}", d.UI.Report)
|
||||
|
||||
r.Get("/events", d.UI.SSE)
|
||||
})
|
||||
// Browser UI — no auth; bind to loopback or LAN only, or front
|
||||
// with a reverse proxy if you need a password.
|
||||
r.Get("/", d.UI.Dashboard)
|
||||
r.Get("/hosts/new", d.UI.NewHostForm)
|
||||
r.Post("/hosts", d.UI.CreateHost)
|
||||
r.Post("/hosts/{id}/delete", d.UI.DeleteHost)
|
||||
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
||||
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
||||
r.Get("/reports/{runID}", d.UI.Report)
|
||||
r.Get("/events", d.UI.SSE)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@ templ Layout(title string) {
|
||||
</nav>
|
||||
<div class="session">
|
||||
<span class="heartbeat" hx-ext="sse" sse-connect="/events" sse-swap="heartbeat">·</span>
|
||||
<form method="post" action="/logout" class="logout-form">
|
||||
<button type="submit">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -42,7 +42,7 @@ func Layout(title string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting</title><link rel=\"stylesheet\" href=\"/static/app.css\"><script src=\"https://unpkg.com/htmx.org@2.0.2\" integrity=\"sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ\" crossorigin=\"anonymous\"></script><script src=\"https://unpkg.com/htmx-ext-sse@2.2.2\" integrity=\"sha384-Y4gc0CK6Kg4hmulDc1rNM+vbMvjbW/5rRCA6pC5gj5dLV1/4+OZGkQpJtHQvQTCr\" crossorigin=\"anonymous\"></script></head><body hx-boost=\"true\"><header class=\"topbar\"><div class=\"brand\">Vetting</div><nav><a href=\"/\">Dashboard</a> <a href=\"/hosts/new\">Register host</a></nav><div class=\"session\"><span class=\"heartbeat\" hx-ext=\"sse\" sse-connect=\"/events\" sse-swap=\"heartbeat\">·</span><form method=\"post\" action=\"/logout\" class=\"logout-form\"><button type=\"submit\">Log out</button></form></div></header><main>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting</title><link rel=\"stylesheet\" href=\"/static/app.css\"><script src=\"https://unpkg.com/htmx.org@2.0.2\" integrity=\"sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ\" crossorigin=\"anonymous\"></script><script src=\"https://unpkg.com/htmx-ext-sse@2.2.2\" integrity=\"sha384-Y4gc0CK6Kg4hmulDc1rNM+vbMvjbW/5rRCA6pC5gj5dLV1/4+OZGkQpJtHQvQTCr\" crossorigin=\"anonymous\"></script></head><body hx-boost=\"true\"><header class=\"topbar\"><div class=\"brand\">Vetting</div><nav><a href=\"/\">Dashboard</a> <a href=\"/hosts/new\">Register host</a></nav><div class=\"session\"><span class=\"heartbeat\" hx-ext=\"sse\" sse-connect=\"/events\" sse-swap=\"heartbeat\">·</span></div></header><main>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func BareLayout(title string) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 41, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 38, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package templates
|
||||
|
||||
templ Login(errMsg, next string) {
|
||||
@BareLayout("Sign in") {
|
||||
<div class="login-card">
|
||||
<h1>Vetting</h1>
|
||||
if errMsg != "" {
|
||||
<div class="error">{ errMsg }</div>
|
||||
}
|
||||
<form method="post" action="/login">
|
||||
<input type="hidden" name="next" value={ next }/>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" autofocus required/>
|
||||
</label>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
package templates
|
||||
|
||||
//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 Login(errMsg, next 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_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"login-card\"><h1>Vetting</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"error\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/login.templ`, Line: 8, Col: 31}
|
||||
}
|
||||
_, 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, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/login\"><input type=\"hidden\" name=\"next\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(next)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/login.templ`, Line: 11, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"> <label>Password <input type=\"password\" name=\"password\" autofocus required></label> <button type=\"submit\">Sign in</button></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = BareLayout("Sign in").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
Reference in New Issue
Block a user