mirror of
https://github.com/1f349/lavender.git
synced 2025-04-14 06:55:55 +01:00
I guess I modified stuff
This commit is contained in:
parent
3fbe905cb2
commit
c27a86010c
@ -1,15 +1,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
)
|
||||
|
||||
type Button interface {
|
||||
// ButtonName defines the text to show on the button.
|
||||
ButtonName() string
|
||||
Provider
|
||||
|
||||
// RenderButtonTemplate returns a template for the button widget.
|
||||
RenderButtonTemplate(ctx context.Context, req *http.Request) template.HTML
|
||||
RenderButtonTemplate(ctx authContext.TemplateContext)
|
||||
}
|
||||
|
13
auth/auth-form.go
Normal file
13
auth/auth-form.go
Normal file
@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import "github.com/1f349/lavender/auth/authContext"
|
||||
|
||||
type Form interface {
|
||||
Provider
|
||||
|
||||
// RenderTemplate returns HTML to embed in the page template.
|
||||
RenderTemplate(ctx authContext.TemplateContext) error
|
||||
|
||||
// AttemptLogin processes the login request.
|
||||
AttemptLogin(ctx authContext.FormContext) error
|
||||
}
|
54
auth/auth.go
54
auth/auth.go
@ -2,11 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
"github.com/1f349/lavender/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// State defines the currently reached authentication state
|
||||
@ -15,6 +11,9 @@ type State byte
|
||||
const (
|
||||
// StateUnauthorized defines the "unauthorized" state of a session
|
||||
StateUnauthorized State = iota
|
||||
// StateBase defines the "username" only user state
|
||||
// This state is for providing a username to allow redirecting to oauth clients
|
||||
StateBase
|
||||
// StateBasic defines the "username and password with no OTP" user state
|
||||
// This is skipped if OTP/passkey is optional and not enabled for the user
|
||||
StateBasic
|
||||
@ -36,53 +35,6 @@ type Provider interface {
|
||||
|
||||
// Name defines a string value for the provider.
|
||||
Name() string
|
||||
|
||||
// RenderTemplate returns HTML to embed in the page template.
|
||||
RenderTemplate(ctx authContext.TemplateContext) error
|
||||
|
||||
// AttemptLogin processes the login request.
|
||||
AttemptLogin(ctx authContext.TemplateContext) error
|
||||
}
|
||||
|
||||
type UserSafeError struct {
|
||||
Display string
|
||||
Code int
|
||||
Internal error
|
||||
}
|
||||
|
||||
func (e UserSafeError) Error() string {
|
||||
return fmt.Sprintf("%s [%d]: %v", e.Display, e.Code, e.Internal)
|
||||
}
|
||||
|
||||
func (e UserSafeError) Unwrap() error {
|
||||
return e.Internal
|
||||
}
|
||||
|
||||
func BasicUserSafeError(code int, message string) UserSafeError {
|
||||
return UserSafeError{
|
||||
Code: code,
|
||||
Display: message,
|
||||
Internal: errors.New(message),
|
||||
}
|
||||
}
|
||||
|
||||
func AdminSafeError(inner error) UserSafeError {
|
||||
return UserSafeError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Display: "Internal server error",
|
||||
Internal: inner,
|
||||
}
|
||||
}
|
||||
|
||||
type RedirectError struct {
|
||||
Target string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e RedirectError) TargetUrl() string { return e.Target }
|
||||
|
||||
func (e RedirectError) Error() string {
|
||||
return fmt.Sprintf("redirect to '%s'", e.Target)
|
||||
}
|
||||
|
||||
type LookupUserDB interface {
|
||||
|
@ -2,11 +2,12 @@ package authContext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1f349/lavender/auth/login-process"
|
||||
"github.com/1f349/lavender/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewTemplateContext(req *http.Request, user *database.User) TemplateContext {
|
||||
func NewTemplateContext(req *http.Request, user *database.User) *BaseTemplateContext {
|
||||
return &BaseTemplateContext{
|
||||
req: req,
|
||||
user: user,
|
||||
@ -18,30 +19,38 @@ type TemplateContext interface {
|
||||
Request() *http.Request
|
||||
User() *database.User
|
||||
Render(data any)
|
||||
Data() any
|
||||
}
|
||||
|
||||
type FormContext interface {
|
||||
Context() context.Context
|
||||
Request() *http.Request
|
||||
User() *database.User
|
||||
SetUser(user *database.User)
|
||||
Render(data any)
|
||||
UpdateSession(data login_process.LoginProcessData)
|
||||
}
|
||||
|
||||
type ButtonContext interface {
|
||||
Context() context.Context
|
||||
Request() *http.Request
|
||||
Render(data any)
|
||||
}
|
||||
|
||||
var _ TemplateContext = &BaseTemplateContext{}
|
||||
|
||||
type BaseTemplateContext struct {
|
||||
req *http.Request
|
||||
user *database.User
|
||||
data any
|
||||
}
|
||||
|
||||
func (t *BaseTemplateContext) Context() context.Context {
|
||||
return t.req.Context()
|
||||
}
|
||||
func (t *BaseTemplateContext) Context() context.Context { return t.req.Context() }
|
||||
|
||||
func (t *BaseTemplateContext) Request() *http.Request {
|
||||
return t.req
|
||||
}
|
||||
func (t *BaseTemplateContext) Request() *http.Request { return t.req }
|
||||
|
||||
func (t *BaseTemplateContext) User() *database.User {
|
||||
return t.user
|
||||
}
|
||||
func (t *BaseTemplateContext) User() *database.User { return t.user }
|
||||
|
||||
func (t *BaseTemplateContext) Render(data any) {
|
||||
t.data = data
|
||||
}
|
||||
func (t *BaseTemplateContext) Render(data any) { t.data = data }
|
||||
|
||||
func (t *BaseTemplateContext) Data() any {
|
||||
return t.data
|
||||
|
14
auth/login-process/login-process-data.go
Normal file
14
auth/login-process/login-process-data.go
Normal file
@ -0,0 +1,14 @@
|
||||
package login_process
|
||||
|
||||
import "github.com/1f349/mjwt"
|
||||
|
||||
var _ mjwt.Claims = (*LoginProcessData)(nil)
|
||||
|
||||
// TODO: add some actual session management
|
||||
type LoginProcessData struct {
|
||||
State byte
|
||||
}
|
||||
|
||||
func (d LoginProcessData) Valid() error { return nil }
|
||||
|
||||
func (d LoginProcessData) Type() string { return "login-process" }
|
47
auth/providers/base.go
Normal file
47
auth/providers/base.go
Normal file
@ -0,0 +1,47 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/1f349/lavender/auth"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
"github.com/1f349/lavender/logger"
|
||||
)
|
||||
|
||||
var _ auth.Provider = (*InitialLogin)(nil)
|
||||
|
||||
type InitialLogin struct{}
|
||||
|
||||
func (m *InitialLogin) AccessState() auth.State { return auth.StateUnauthorized }
|
||||
|
||||
func (m *InitialLogin) Name() string { return "base" }
|
||||
|
||||
func (m *InitialLogin) RenderTemplate(ctx authContext.FormContext) error {
|
||||
type s struct {
|
||||
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
|
||||
}
|
||||
|
||||
ctx.Render(s{
|
||||
UserEmail: "",
|
||||
Redirect: q.Get("redirect"),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
return nil
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/1f349/lavender/auth"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
"github.com/1f349/lavender/web"
|
||||
)
|
||||
|
||||
var _ auth.Provider = (*MemoryLogin)(nil)
|
||||
|
||||
type MemoryLogin struct{}
|
||||
|
||||
func (m *MemoryLogin) AccessState() auth.State { return auth.StateUnauthorized }
|
||||
|
||||
func (m *MemoryLogin) Name() string { return "memory" }
|
||||
|
||||
func (m *MemoryLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
cookie, err := ctx.Request().Cookie("lavender-user-memory")
|
||||
if err == nil && cookie.Valid() == nil {
|
||||
ctx.Render(struct {
|
||||
ServiceName string
|
||||
LoginName string
|
||||
Redirect string
|
||||
}{
|
||||
ServiceName: ,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MemoryLogin) AttemptLogin(ctx authContext.TemplateContext) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
@ -47,13 +47,7 @@ func (o OAuthLogin) AccessState() auth.State { return auth.StateUnauthorized }
|
||||
|
||||
func (o OAuthLogin) Name() string { return "oauth" }
|
||||
|
||||
func (o OAuthLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
// TODO: does this need to exist?
|
||||
ctx.Render(map[string]any{"Error": "no"})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o OAuthLogin) AttemptLogin(ctx authContext.TemplateContext) error {
|
||||
func (o OAuthLogin) AttemptLogin(ctx authContext.FormContext) error {
|
||||
rCtx := ctx.Context()
|
||||
|
||||
login, ok := rCtx.Value(oauthServiceLogin(0)).(*issuer.WellKnownOIDC)
|
||||
|
@ -30,7 +30,7 @@ func (o *OtpLogin) AccessState() auth.State { return auth.StateBasic }
|
||||
|
||||
func (o *OtpLogin) Name() string { return "basic" }
|
||||
|
||||
func (o *OtpLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
func (o *OtpLogin) RenderTemplate(ctx authContext.FormContext) error {
|
||||
user := ctx.User()
|
||||
if user == nil || user.Subject == "" {
|
||||
return fmt.Errorf("requires previous factor")
|
||||
@ -48,7 +48,7 @@ func (o *OtpLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OtpLogin) AttemptLogin(ctx authContext.TemplateContext) error {
|
||||
func (o *OtpLogin) AttemptLogin(ctx authContext.FormContext) error {
|
||||
user := ctx.User()
|
||||
if user == nil || user.Subject == "" {
|
||||
return fmt.Errorf("requires previous factor")
|
||||
|
@ -2,9 +2,7 @@ package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/1f349/lavender/auth"
|
||||
"github.com/1f349/lavender/auth/authContext"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
@ -26,38 +24,6 @@ func (p *PasskeyLogin) AccessState() auth.State { return auth.StateUnauthorized
|
||||
|
||||
func (p *PasskeyLogin) Name() string { return "passkey" }
|
||||
|
||||
func (p *PasskeyLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
user := ctx.User()
|
||||
if user == nil || user.Subject == "" {
|
||||
return fmt.Errorf("requires previous factor")
|
||||
}
|
||||
if user.OtpSecret == "" {
|
||||
return fmt.Errorf("user does not support factor")
|
||||
}
|
||||
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
var passkeyShortcut = true
|
||||
|
||||
func init() {
|
||||
passkeyShortcut = true
|
||||
}
|
||||
|
||||
func (p *PasskeyLogin) AttemptLogin(ctx authContext.TemplateContext) error {
|
||||
user := ctx.User()
|
||||
if user.Subject == "" && !passkeyShortcut {
|
||||
return fmt.Errorf("requires previous factor")
|
||||
}
|
||||
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (p *PasskeyLogin) ButtonName() string {
|
||||
return "Login with Passkey"
|
||||
}
|
||||
|
||||
func (p *PasskeyLogin) RenderButtonTemplate(ctx context.Context, req *http.Request) template.HTML {
|
||||
return "<div>Passkey Button</div>"
|
||||
}
|
||||
|
@ -10,22 +10,25 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type basicLoginDB interface {
|
||||
type passwordLoginDB interface {
|
||||
auth.LookupUserDB
|
||||
CheckLogin(ctx context.Context, un, pw string) (database.CheckLoginResult, error)
|
||||
}
|
||||
|
||||
var _ auth.Provider = (*BasicLogin)(nil)
|
||||
var (
|
||||
_ auth.Provider = (*PasswordLogin)(nil)
|
||||
_ auth.Form = (*PasswordLogin)(nil)
|
||||
)
|
||||
|
||||
type BasicLogin struct {
|
||||
DB basicLoginDB
|
||||
type PasswordLogin struct {
|
||||
DB passwordLoginDB
|
||||
}
|
||||
|
||||
func (b *BasicLogin) AccessState() auth.State { return auth.StateUnauthorized }
|
||||
func (b *PasswordLogin) AccessState() auth.State { return auth.StateBase }
|
||||
|
||||
func (b *BasicLogin) Name() string { return "basic" }
|
||||
func (b *PasswordLogin) Name() string { return "password" }
|
||||
|
||||
func (b *BasicLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
func (b *PasswordLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
// TODO(melon): rewrite this
|
||||
req := ctx.Request()
|
||||
un := req.FormValue("login")
|
||||
@ -43,7 +46,7 @@ func (b *BasicLogin) RenderTemplate(ctx authContext.TemplateContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BasicLogin) AttemptLogin(ctx authContext.TemplateContext) error {
|
||||
func (b *PasswordLogin) AttemptLogin(ctx authContext.FormContext) error {
|
||||
req := ctx.Request()
|
||||
un := req.FormValue("username")
|
||||
pw := req.FormValue("password")
|
14
auth/redirect-error.go
Normal file
14
auth/redirect-error.go
Normal file
@ -0,0 +1,14 @@
|
||||
package auth
|
||||
|
||||
import "fmt"
|
||||
|
||||
type RedirectError struct {
|
||||
Target string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e RedirectError) TargetUrl() string { return e.Target }
|
||||
|
||||
func (e RedirectError) Error() string {
|
||||
return fmt.Sprintf("redirect to '%s'", e.Target)
|
||||
}
|
37
auth/user-safe-error.go
Normal file
37
auth/user-safe-error.go
Normal file
@ -0,0 +1,37 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type UserSafeError struct {
|
||||
Display string
|
||||
Code int
|
||||
Internal error
|
||||
}
|
||||
|
||||
func (e UserSafeError) Error() string {
|
||||
return fmt.Sprintf("%s [%d]: %v", e.Display, e.Code, e.Internal)
|
||||
}
|
||||
|
||||
func (e UserSafeError) Unwrap() error {
|
||||
return e.Internal
|
||||
}
|
||||
|
||||
func BasicUserSafeError(code int, message string) UserSafeError {
|
||||
return UserSafeError{
|
||||
Code: code,
|
||||
Display: message,
|
||||
Internal: errors.New(message),
|
||||
}
|
||||
}
|
||||
|
||||
func AdminSafeError(inner error) UserSafeError {
|
||||
return UserSafeError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Display: "Internal server error",
|
||||
Internal: inner,
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package database
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: manage-oauth.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: manage-users.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package database
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: otp.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: profiles.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: roles.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: users.sql
|
||||
|
||||
package database
|
||||
|
@ -54,7 +54,11 @@ func (h *httpServer) testAuthSources(req *http.Request, user *database.User, fac
|
||||
if i.AccessState() != factor {
|
||||
continue
|
||||
}
|
||||
err := i.RenderTemplate(authContext.NewTemplateContext(req, user))
|
||||
form, ok := i.(auth.Form)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := form.RenderTemplate(authContext.NewTemplateContext(req, user))
|
||||
authSource[i.Name()] = err == nil
|
||||
clear(data)
|
||||
}
|
||||
@ -70,7 +74,7 @@ func (h *httpServer) getAuthWithState(state auth.State) auth.Provider {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Provider) (template.HTML, error) {
|
||||
func (h *httpServer) renderAuthTemplate(req *http.Request, provider auth.Form) (template.HTML, error) {
|
||||
tmpCtx := authContext.NewTemplateContext(req, new(database.User))
|
||||
|
||||
err := provider.RenderTemplate(tmpCtx)
|
||||
@ -119,9 +123,18 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
return
|
||||
}
|
||||
|
||||
buttonTemplates := make([]template.HTML, len(h.authButtons))
|
||||
buttonCtx := authContext.NewTemplateContext(req, new(database.User))
|
||||
|
||||
buttonTemplates := make([]template.HTML, 0, len(h.authButtons))
|
||||
for i := range h.authButtons {
|
||||
buttonTemplates[i] = h.authButtons[i].RenderButtonTemplate(req.Context(), req)
|
||||
h.authButtons[i].RenderButtonTemplate(buttonCtx)
|
||||
if buttonCtx.Data() != nil {
|
||||
// TODO: finish the buttons here
|
||||
buf := new(bytes.Buffer)
|
||||
web.RenderPageTemplate(buf, "auth-buttons/"+h.authButtons[i].Name(), buttonCtx.Data())
|
||||
buttonTemplates = append(buttonTemplates, template.HTML(buf.String()))
|
||||
buttonCtx.Render(nil)
|
||||
}
|
||||
}
|
||||
|
||||
type loginError struct {
|
||||
@ -133,8 +146,9 @@ func (h *httpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
||||
provider := h.getAuthWithState(auth.StateUnauthorized)
|
||||
|
||||
// Maybe the admin has disabled some login providers but does have a button based provider available?
|
||||
if provider != nil {
|
||||
renderTemplate, err = h.renderAuthTemplate(req, provider)
|
||||
form, ok := provider.(auth.Form)
|
||||
if provider != nil && ok {
|
||||
renderTemplate, err = h.renderAuthTemplate(req, form)
|
||||
if err != nil {
|
||||
logger.Logger.Warn("No provider for login")
|
||||
web.RenderPageTemplate(rw, "login-error", loginError{Error: "No available provider for login"})
|
||||
|
@ -55,7 +55,7 @@ type mailLinkKey struct {
|
||||
func SetupRouter(r *httprouter.Router, config conf.Conf, mailSender *mail.Mail, db *database.Queries, signingKey *mjwt.Issuer) {
|
||||
// TODO: move auth provider init to main function
|
||||
// TODO: allow dynamically changing the providers based on database information
|
||||
authBasic := &providers.BasicLogin{DB: db}
|
||||
authBasic := &providers.PasswordLogin{DB: db}
|
||||
authOtp := &providers.OtpLogin{DB: db}
|
||||
authOAuth := &providers.OAuthLogin{DB: db, BaseUrl: &config.BaseUrl}
|
||||
authOAuth.Init()
|
||||
|
@ -8,5 +8,8 @@ export const partial = true;
|
||||
<label for="field_email">Email:</label>
|
||||
<input type="email" name="email" id="field_email" value="[[.UserEmail]]" required/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Remember Me? <input type="checkbox" name="remember-me"/></label>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
@ -4,10 +4,7 @@ export const partial = true;
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<input type="hidden" name="redirect" value="[[.Redirect]]"/>
|
||||
<div>
|
||||
<label for="field_email">Email:</label>
|
||||
<input type="email" name="email" id="field_email" value="[[.UserEmail]]" required/>
|
||||
</div>
|
||||
<input type="hidden" name="email" value="[[.UserEmail]]"/>
|
||||
<div>
|
||||
<label for="field_password">Password:</label>
|
||||
<input type="password" name="password" id="field_password" autofocus required/>
|
Loading…
x
Reference in New Issue
Block a user