mirror of
https://github.com/1f349/lavender.git
synced 2025-04-14 06:55:55 +01:00
Improved login process and allow hijacking the login process
This commit is contained in:
parent
80d3298813
commit
d9b0074133
@ -20,8 +20,10 @@ type FormContext interface {
|
||||
TemplateContext
|
||||
SetUser(user *database.User)
|
||||
UpdateSession(data process.LoginProcessData)
|
||||
GetLoginProcessData() process.LoginProcessData
|
||||
ResponseWriter() http.ResponseWriter
|
||||
__formContext()
|
||||
Hijack()
|
||||
}
|
||||
|
||||
var _ FormContext = &BaseFormContext{}
|
||||
@ -30,22 +32,19 @@ type BaseFormContext struct {
|
||||
BaseTemplateContext
|
||||
loginProcessData process.LoginProcessData
|
||||
rw http.ResponseWriter
|
||||
hijack bool
|
||||
}
|
||||
|
||||
func (b *BaseFormContext) GetLoginProcessData() process.LoginProcessData {
|
||||
return b.loginProcessData
|
||||
}
|
||||
func (b *BaseFormContext) GetLoginProcessData() process.LoginProcessData { return b.loginProcessData }
|
||||
|
||||
func (b *BaseFormContext) SetUser(user *database.User) {
|
||||
b.BaseTemplateContext.user = user
|
||||
}
|
||||
func (b *BaseFormContext) SetUser(user *database.User) { b.BaseTemplateContext.user = user }
|
||||
|
||||
func (b *BaseFormContext) UpdateSession(data process.LoginProcessData) {
|
||||
b.loginProcessData = data
|
||||
}
|
||||
func (b *BaseFormContext) UpdateSession(data process.LoginProcessData) { b.loginProcessData = data }
|
||||
|
||||
func (b *BaseFormContext) ResponseWriter() http.ResponseWriter {
|
||||
return b.rw
|
||||
}
|
||||
func (b *BaseFormContext) ResponseWriter() http.ResponseWriter { return b.rw }
|
||||
|
||||
func (b *BaseFormContext) __formContext() {}
|
||||
|
||||
func (b *BaseFormContext) Hijack() { b.hijack = true }
|
||||
|
||||
func (b *BaseFormContext) HijackCalled() bool { return b.hijack }
|
||||
|
@ -2,14 +2,16 @@ package authContext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1f349/lavender/auth/process"
|
||||
"github.com/1f349/lavender/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewTemplateContext(req *http.Request, user *database.User) *BaseTemplateContext {
|
||||
func NewTemplateContext(req *http.Request, user *database.User, processData process.LoginProcessData) *BaseTemplateContext {
|
||||
return &BaseTemplateContext{
|
||||
req: req,
|
||||
user: user,
|
||||
req: req,
|
||||
user: user,
|
||||
processData: processData,
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,15 +20,17 @@ type TemplateContext interface {
|
||||
Request() *http.Request
|
||||
User() *database.User
|
||||
Render(data any)
|
||||
LoginProcessData() process.LoginProcessData
|
||||
__templateContext()
|
||||
}
|
||||
|
||||
var _ TemplateContext = &BaseTemplateContext{}
|
||||
|
||||
type BaseTemplateContext struct {
|
||||
req *http.Request
|
||||
user *database.User
|
||||
data any
|
||||
req *http.Request
|
||||
user *database.User
|
||||
processData process.LoginProcessData
|
||||
data any
|
||||
}
|
||||
|
||||
func (t *BaseTemplateContext) Context() context.Context { return t.req.Context() }
|
||||
@ -37,6 +41,8 @@ func (t *BaseTemplateContext) User() *database.User { return t.user }
|
||||
|
||||
func (t *BaseTemplateContext) Render(data any) { t.data = data }
|
||||
|
||||
func (t *BaseTemplateContext) LoginProcessData() process.LoginProcessData { return t.processData }
|
||||
|
||||
func (t *BaseTemplateContext) Data() any { return t.data }
|
||||
|
||||
func (t *BaseTemplateContext) __templateContext() {}
|
||||
|
@ -6,9 +6,15 @@ import (
|
||||
|
||||
var _ mjwt.Claims = (*LoginProcessData)(nil)
|
||||
|
||||
// LoginProcessData stores the current state and relevant information during the
|
||||
// process of a login. This data is sent signed but unencrypted to the user's
|
||||
// device. For this reason, all fields must only contain user input or generic
|
||||
// enum state data.
|
||||
//
|
||||
// TODO: add some actual session management
|
||||
type LoginProcessData struct {
|
||||
State State
|
||||
Email string
|
||||
}
|
||||
|
||||
func (d LoginProcessData) Valid() error { return nil }
|
||||
|
@ -19,6 +19,10 @@ const (
|
||||
StateSudo
|
||||
)
|
||||
|
||||
func (s State) IsValid() bool {
|
||||
return s <= StateSudo
|
||||
}
|
||||
|
||||
func (s State) IsLoggedIn() bool { return s >= StateExtended }
|
||||
|
||||
func (s State) IsSudoAvailable() bool { return s == StateSudo }
|
||||
|
@ -1,20 +1,30 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/lavender/auth"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
process "github.com/1f349/lavender/auth/process"
|
||||
"github.com/1f349/lavender/database"
|
||||
"github.com/1f349/lavender/issuer"
|
||||
"github.com/1f349/lavender/logger"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const memoryCookieName = "lavender-user-memory"
|
||||
|
||||
var _ auth.Provider = (*InitialLogin)(nil)
|
||||
var _ auth.Form = (*InitialLogin)(nil)
|
||||
|
||||
type InitialLogin struct {
|
||||
DB *database.Queries
|
||||
DB *database.Queries
|
||||
MyNamespace string
|
||||
Manager *issuer.Manager
|
||||
}
|
||||
|
||||
func (m *InitialLogin) AccessState() process.State { return process.StateUnauthorized }
|
||||
@ -23,23 +33,24 @@ func (m *InitialLogin) Name() string { return "base" }
|
||||
|
||||
func (m *InitialLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
type s struct {
|
||||
UserEmail string
|
||||
Redirect string
|
||||
LoginNameMemory bool
|
||||
UserEmail string
|
||||
Redirect string
|
||||
}
|
||||
|
||||
req := ctx.Request()
|
||||
q := req.URL.Query()
|
||||
cookie, err := req.Cookie("lavender-user-memory")
|
||||
if err == nil && cookie.Valid() == nil {
|
||||
ctx.Render(s{
|
||||
UserEmail: cookie.Value,
|
||||
Redirect: q.Get("redirect"),
|
||||
})
|
||||
return nil
|
||||
|
||||
userEmail := ctx.LoginProcessData().Email
|
||||
if userEmail == "" {
|
||||
cookie, err := req.Cookie(memoryCookieName)
|
||||
if err == nil && cookie.Valid() == nil {
|
||||
userEmail = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Render(s{
|
||||
UserEmail: "",
|
||||
UserEmail: userEmail,
|
||||
Redirect: q.Get("redirect"),
|
||||
})
|
||||
return nil
|
||||
@ -47,24 +58,85 @@ func (m *InitialLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
|
||||
func (m *InitialLogin) AttemptLogin(ctx authContext.FormContext) error {
|
||||
req := ctx.Request()
|
||||
userEmail := req.FormValue("email")
|
||||
rememberMe := req.FormValue("remember-me")
|
||||
logger.Logger.Debug("Hi", "em", userEmail, "rm", rememberMe)
|
||||
|
||||
rw := ctx.ResponseWriter()
|
||||
now := time.Now()
|
||||
future := now.AddDate(1, 0, 0)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "lavender-user-memory",
|
||||
Value: userEmail,
|
||||
Path: "/",
|
||||
Expires: future,
|
||||
MaxAge: int(future.Sub(now).Seconds()),
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
ctx.UpdateSession(process.LoginProcessData{State: process.StateBase})
|
||||
if req.FormValue("not-you") != "" {
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: memoryCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
return auth.RedirectError{
|
||||
Target: "/login",
|
||||
Code: http.StatusFound,
|
||||
}
|
||||
}
|
||||
|
||||
loginName := req.FormValue("email")
|
||||
if loginName == "" {
|
||||
return errors.New("email required")
|
||||
}
|
||||
|
||||
rememberMe := req.FormValue("remember-me") != ""
|
||||
logger.Logger.Debug("Hi", "login", loginName, "remember", rememberMe)
|
||||
|
||||
// Set the remember me cookie
|
||||
if rememberMe {
|
||||
now := time.Now()
|
||||
future := now.AddDate(1, 0, 0)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: memoryCookieName,
|
||||
Value: loginName,
|
||||
Path: "/",
|
||||
Expires: future,
|
||||
MaxAge: int(future.Sub(now).Seconds()),
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// append local namespace if @ is missing
|
||||
n := strings.IndexByte(loginName, '@')
|
||||
if n < 0 {
|
||||
// correct the @ index
|
||||
n = len(loginName)
|
||||
loginName += "@" + m.MyNamespace
|
||||
}
|
||||
|
||||
login := m.Manager.FindServiceFromLogin(loginName)
|
||||
if login == nil {
|
||||
http.Error(rw, "No login service defined for this username", http.StatusBadRequest)
|
||||
return errors.New("no login service defined for this username")
|
||||
}
|
||||
|
||||
// the @ must exist if the service is defined
|
||||
loginUn := loginName[:n]
|
||||
|
||||
// TODO: finish migrating this shit
|
||||
|
||||
// the login is not for this namespace
|
||||
if login != issuer.MeWellKnown {
|
||||
// save state for use later
|
||||
state := login.Config.Namespace + ":" + uuid.NewString()
|
||||
h.flowState.Set(state, flowStateData{loginName, login, req.PostFormValue("redirect")}, time.Now().Add(15*time.Minute))
|
||||
|
||||
// generate oauth2 config and redirect to authorize URL
|
||||
oa2conf := login.OAuth2Config
|
||||
oa2conf.RedirectURL = h.conf.BaseUrl.JoinPath("callback").String()
|
||||
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
|
||||
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.UpdateSession(process.LoginProcessData{
|
||||
Email: loginName,
|
||||
State: process.StateBase,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
133
server/login.go
133
server/login.go
@ -8,7 +8,6 @@ import (
|
||||
"github.com/1f349/lavender/auth"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
"github.com/1f349/lavender/auth/process"
|
||||
"github.com/1f349/lavender/auth/providers"
|
||||
"github.com/1f349/lavender/database"
|
||||
"github.com/1f349/lavender/issuer"
|
||||
"github.com/1f349/lavender/logger"
|
||||
@ -23,7 +22,6 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -52,8 +50,8 @@ func (h *httpServer) getAuthWithState(state process.State) auth.Provider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Form) (template.HTML, error) {
|
||||
tmpCtx := authContext.NewTemplateContext(req, new(database.User))
|
||||
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Form, processData process.LoginProcessData) (template.HTML, error) {
|
||||
tmpCtx := authContext.NewTemplateContext(req, new(database.User), processData)
|
||||
|
||||
err := provider.RenderTemplate(tmpCtx)
|
||||
if err != nil {
|
||||
@ -73,9 +71,16 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
return
|
||||
}
|
||||
|
||||
var processData process.LoginProcessData
|
||||
|
||||
jwtCookie, err := readJwtCookie[process.LoginProcessData](req, "lavender-login-process", h.signingKey.KeyStore())
|
||||
if err == nil {
|
||||
processData = jwtCookie.Claims
|
||||
}
|
||||
|
||||
// TODO: some of this should be more like tulip
|
||||
|
||||
buttonCtx := authContext.NewTemplateContext(req, new(database.User))
|
||||
buttonCtx := authContext.NewTemplateContext(req, new(database.User), processData)
|
||||
|
||||
buttonTemplates := make([]template.HTML, 0, len(h.authButtons))
|
||||
for i := range h.authButtons {
|
||||
@ -89,22 +94,14 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
}
|
||||
}
|
||||
|
||||
authState := process.StateUnauthorized
|
||||
|
||||
jwtCookie, err := readJwtCookie[process.LoginProcessData](req, "login-process", h.signingKey.KeyStore())
|
||||
if err == nil {
|
||||
authState = jwtCookie.Claims.State
|
||||
return
|
||||
}
|
||||
|
||||
provider := h.getAuthWithState(authState)
|
||||
provider := h.getAuthWithState(processData.State)
|
||||
|
||||
var renderTemplate template.HTML
|
||||
|
||||
// Maybe the admin has disabled some login providers but does have a button based provider available?
|
||||
form, ok := provider.(auth.Form)
|
||||
if provider != nil && ok {
|
||||
renderTemplate, err = h.renderAuthTemplate(req, form)
|
||||
renderTemplate, err = h.renderAuthTemplate(req, form, processData)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("No provider for login")
|
||||
web.RenderPageTemplate(rw, "login-error", struct {
|
||||
@ -131,106 +128,40 @@ func (h *httpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ http
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: some of this should be more like tulip
|
||||
|
||||
if req.PostFormValue("not-you") == "1" {
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "lavender-login-name",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
http.Redirect(rw, req, (&url.URL{
|
||||
Path: "/login",
|
||||
}).String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
loginName := req.PostFormValue("email")
|
||||
|
||||
// append local namespace if @ is missing
|
||||
n := strings.IndexByte(loginName, '@')
|
||||
if n < 0 {
|
||||
// correct the @ index
|
||||
n = len(loginName)
|
||||
loginName += "@" + h.conf.Namespace
|
||||
}
|
||||
|
||||
login := h.manager.FindServiceFromLogin(loginName)
|
||||
if login == nil {
|
||||
http.Error(rw, "No login service defined for this username", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// the @ must exist if the service is defined
|
||||
loginUn := loginName[:n]
|
||||
|
||||
ctx := providers.WithWellKnown(req.Context(), login)
|
||||
ctx = context.WithValue(ctx, "login_username", loginUn)
|
||||
ctx = context.WithValue(ctx, "login_full", loginName)
|
||||
|
||||
// TODO(melon): only do if remember-me is enabled
|
||||
now := time.Now()
|
||||
future := now.AddDate(1, 0, 0)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "lavender-login-name",
|
||||
Value: loginName,
|
||||
Path: "/",
|
||||
Expires: future,
|
||||
MaxAge: int(future.Sub(now).Seconds()),
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
var redirectError auth.RedirectError
|
||||
|
||||
// TODO(melon): rewrite login system here
|
||||
|
||||
// if the login is the local server
|
||||
if login != issuer.MeWellKnown {
|
||||
// save state for use later
|
||||
state := login.Config.Namespace + ":" + uuid.NewString()
|
||||
h.flowState.Set(state, flowStateData{loginName, login, req.PostFormValue("redirect")}, time.Now().Add(15*time.Minute))
|
||||
|
||||
// generate oauth2 config and redirect to authorize URL
|
||||
oa2conf := login.OAuth2Config
|
||||
oa2conf.RedirectURL = h.conf.BaseUrl.JoinPath("callback").String()
|
||||
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
|
||||
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
var authForm auth.Form
|
||||
|
||||
{
|
||||
for _, i := range h.authSources {
|
||||
if form, ok := i.(auth.Form); ok {
|
||||
if req.PostFormValue("provider") == form.Name() {
|
||||
authForm = form
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
var processData process.LoginProcessData
|
||||
jwtCookie, err := readJwtCookie[process.LoginProcessData](req, "lavender-login-process", h.signingKey.KeyStore())
|
||||
if err == nil {
|
||||
processData = jwtCookie.Claims
|
||||
}
|
||||
|
||||
authForm := h.formProviderLookup[req.PostFormValue("provider")]
|
||||
if authForm == nil {
|
||||
http.Error(rw, "Invalid auth provider", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if processData.State != authForm.AccessState() {
|
||||
http.Redirect(rw, req, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: rewrite
|
||||
formContext := authContext.NewFormContext(req, nil, rw)
|
||||
err := authForm.AttemptLogin(formContext)
|
||||
switch {
|
||||
case errors.As(err, &redirectError):
|
||||
err = authForm.AttemptLogin(formContext)
|
||||
|
||||
var redirectError auth.RedirectError
|
||||
if errors.As(err, &redirectError) {
|
||||
http.Redirect(rw, req, redirectError.Target, redirectError.Code)
|
||||
return
|
||||
}
|
||||
|
||||
// ResponseWriter has been hijacked so we stop processing here
|
||||
if formContext.HijackCalled() {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: idk why login process data isn't working properly
|
||||
processData := formContext.GetLoginProcessData()
|
||||
processData = formContext.GetLoginProcessData()
|
||||
if h.setLoginProcessCookie(rw, processData) {
|
||||
return
|
||||
}
|
||||
|
@ -38,8 +38,9 @@ type httpServer struct {
|
||||
// flowState contains the flow state of 3rd party oauth2
|
||||
flowState *cache.Cache[string, flowStateData]
|
||||
|
||||
authSources []auth.Provider
|
||||
authButtons []auth.Button
|
||||
authSources []auth.Provider
|
||||
authButtons []auth.Button
|
||||
formProviderLookup map[string]auth.Form
|
||||
}
|
||||
|
||||
type flowStateData struct {
|
||||
@ -71,20 +72,6 @@ func SetupRouter(r *httprouter.Router, config conf.Conf, mailSender *mail.Mail,
|
||||
authOAuth.Init()
|
||||
authPasskey := &providers.PasskeyLogin{DB: db}
|
||||
|
||||
authSources := []auth.Provider{
|
||||
authInitial,
|
||||
authPassword,
|
||||
authOtp,
|
||||
authOAuth,
|
||||
authPasskey,
|
||||
}
|
||||
authButtons := make([]auth.Button, 0)
|
||||
for _, source := range authSources {
|
||||
if button, isButton := source.(auth.Button); isButton {
|
||||
authButtons = append(authButtons, button)
|
||||
}
|
||||
}
|
||||
|
||||
hs := &httpServer{
|
||||
r: r,
|
||||
db: db,
|
||||
@ -94,8 +81,26 @@ func SetupRouter(r *httprouter.Router, config conf.Conf, mailSender *mail.Mail,
|
||||
|
||||
mailLinkCache: cache.New[mailLinkKey, string](),
|
||||
|
||||
authSources: authSources,
|
||||
authButtons: authButtons,
|
||||
authSources: []auth.Provider{
|
||||
authInitial,
|
||||
authPassword,
|
||||
authOtp,
|
||||
authOAuth,
|
||||
authPasskey,
|
||||
},
|
||||
authButtons: make([]auth.Button, 0),
|
||||
formProviderLookup: make(map[string]auth.Form),
|
||||
}
|
||||
|
||||
// build slices and maps for quick access to auth interfaces
|
||||
for _, source := range hs.authSources {
|
||||
if button, isButton := source.(auth.Button); isButton {
|
||||
hs.authButtons = append(hs.authButtons, button)
|
||||
}
|
||||
|
||||
if form, isForm := source.(auth.Form); isForm {
|
||||
hs.formProviderLookup[form.Name()] = form
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -2,6 +2,23 @@
|
||||
export const partial = true;
|
||||
---
|
||||
|
||||
[[ if .LoginNameMemory ]]
|
||||
<div>Login in as: <span>[[ .UserEmail ]]</span></div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="provider" value="base"/>
|
||||
<button type="submit" name="not-you" value="1">Not You?</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="provider" value="base"/>
|
||||
<input type="hidden" name="redirect" value="[[ .Redirect ]]"/>
|
||||
<input type="hidden" name="email" value="[[ .UserEmail ]]"/>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
[[ else ]]
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="provider" value="base"/>
|
||||
<input type="hidden" name="redirect" value="[[.Redirect]]"/>
|
||||
@ -14,3 +31,4 @@ export const partial = true;
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
[[ end ]]
|
||||
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div>Log in as: <span>[[ .LoginName ]]</span></div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<button type="submit" name="not-you" value="1">Not You?</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect" value="[[ .Redirect ]]"/>
|
||||
<input type="hidden" name="loginname" value="[[ .LoginName ]]"/>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
@ -9,16 +9,6 @@ import Layout from "../layouts/Layout.astro";
|
||||
<p>Check your inbox for a verification email</p>
|
||||
[[ end ]]
|
||||
[[ .AuthTemplate ]]
|
||||
<div class="w-full">
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="provider" value="base"/>
|
||||
<input type="hidden" name="redirect" value="[[ .Redirect ]]"/>
|
||||
<div class="w-full">
|
||||
<label>Email: <input type="email" name="email" value="[[ .UserEmail ]]" required/></label>
|
||||
</div>
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
[[ if gt (len .AuthButtons) 0 ]]
|
||||
<div class="auth-buttons">
|
||||
[[ range $authButton := .AuthButtons ]]
|
||||
|
Loading…
x
Reference in New Issue
Block a user