From c6f77e86fecec9550bfd4d95ccd887b2f03f1a7d Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Sun, 8 Jan 2023 17:16:38 +0100 Subject: [PATCH] Add autocomplete attribute to forms --- internal/dis/component/auth/panel/password.go | 13 ++-- internal/dis/component/auth/panel/totp.go | 23 +++---- internal/dis/component/auth/session.go | 11 ++-- .../dis/component/control/admin/grants.go | 3 +- internal/dis/component/control/admin/users.go | 13 ++-- pkg/httpx/field/autocomplete.go | 63 +++++++++++++++++++ pkg/httpx/field/field.go | 40 ++++++++++++ pkg/httpx/field/type.go | 30 +++++++++ pkg/httpx/form.go | 45 +------------ 9 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 pkg/httpx/field/autocomplete.go create mode 100644 pkg/httpx/field/field.go create mode 100644 pkg/httpx/field/type.go diff --git a/internal/dis/component/auth/panel/password.go b/internal/dis/component/auth/panel/password.go index 4c614d2..ea4022e 100644 --- a/internal/dis/component/auth/panel/password.go +++ b/internal/dis/component/auth/panel/password.go @@ -9,6 +9,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" ) //go:embed "templates/password.html" @@ -28,13 +29,13 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler { passwordTemplate := panel.Dependencies.Custom.Template(passwordTemplate) return &httpx.Form[struct{}]{ - Fields: []httpx.Field{ - {Name: "old", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode (optional)"}, - {Name: "new", Type: httpx.PasswordField, EmptyOnError: true, Label: "New Password"}, - {Name: "new2", Type: httpx.PasswordField, EmptyOnError: true, Label: "New Password (again)"}, + Fields: []field.Field{ + {Name: "old", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, + {Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Current Passcode (optional)"}, + {Name: "new", Type: field.Password, Autocomplete: field.NewPassword, EmptyOnError: true, Label: "New Password"}, + {Name: "new2", Type: field.Password, Autocomplete: field.NewPassword, EmptyOnError: true, Label: "New Password (again)"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, RenderTemplate: passwordTemplate, RenderTemplateContext: panel.UserFormContext, diff --git a/internal/dis/component/auth/panel/totp.go b/internal/dis/component/auth/panel/totp.go index 5769879..f18acd6 100644 --- a/internal/dis/component/auth/panel/totp.go +++ b/internal/dis/component/auth/panel/totp.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" _ "embed" ) @@ -20,10 +21,10 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { totpEnableTemplate := panel.Dependencies.Custom.Template(totpEnableTemplate) return &httpx.Form[struct{}]{ - Fields: []httpx.Field{ - {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, + Fields: []field.Field{ + {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, SkipForm: func(r *http.Request) (data struct{}, skip bool) { user, err := panel.Dependencies.Auth.UserOf(r) @@ -78,11 +79,11 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { totpEnrollTemplate := panel.Dependencies.Custom.Template(totpEnrollTemplate) return &httpx.Form[struct{}]{ - Fields: []httpx.Field{ - {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode"}, + Fields: []field.Field{ + {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, + {Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Passcode"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, SkipForm: func(r *http.Request) (data struct{}, skip bool) { user, err := panel.Dependencies.Auth.UserOf(r) @@ -152,11 +153,11 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { totpDisableTemplate := panel.Dependencies.Custom.Template(totpDisableTemplate) return &httpx.Form[struct{}]{ - Fields: []httpx.Field{ - {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode"}, + Fields: []field.Field{ + {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"}, + {Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Current Passcode"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, SkipForm: func(r *http.Request) (data struct{}, skip bool) { user, err := panel.Dependencies.Auth.UserOf(r) diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index bd650a2..4b1192a 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/gorilla/sessions" _ "embed" @@ -114,12 +115,12 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { loginTemplate := auth.Dependencies.Custom.Template(loginTemplate) return &httpx.Form[*AuthUser]{ - Fields: []httpx.Field{ - {Name: "username", Type: httpx.TextField, Label: "Username"}, - {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Password"}, - {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode (optional)"}, + Fields: []field.Field{ + {Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"}, + {Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Password"}, + {Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Passcode (optional)"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { if context.Err != nil { diff --git a/internal/dis/component/control/admin/grants.go b/internal/dis/component/control/admin/grants.go index 3e12c57..bbdb733 100644 --- a/internal/dis/component/control/admin/grants.go +++ b/internal/dis/component/control/admin/grants.go @@ -11,6 +11,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/gorilla/mux" "golang.org/x/exp/maps" "golang.org/x/exp/slices" @@ -116,7 +117,7 @@ func (admin *Admin) postGrants(r *http.Request) (gc grantsContext, err error) { delete = r.PostFormValue("action") == "delete" distilleryUser = r.PostFormValue("distillery-user") drupalUser = r.PostFormValue("drupal-user") - adminRole = r.PostFormValue("admin") == httpx.CheckboxChecked + adminRole = r.PostFormValue("admin") == field.CheckboxChecked ) // set the common fields diff --git a/internal/dis/component/control/admin/users.go b/internal/dis/component/control/admin/users.go index 2aea17b..c41be19 100644 --- a/internal/dis/component/control/admin/users.go +++ b/internal/dis/component/control/admin/users.go @@ -11,6 +11,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/rs/zerolog" ) @@ -57,18 +58,18 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler { userCreateTemplate := admin.Dependencies.Custom.Template(userCreateTemplate) return &httpx.Form[createUserResult]{ - Fields: []httpx.Field{ - {Name: "username", Type: httpx.TextField, Label: "Username"}, - {Name: "password", Type: httpx.PasswordField, Label: "Password"}, - {Name: "admin", Type: httpx.CheckboxField, Label: "Distillery Administrator"}, + Fields: []field.Field{ + {Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"}, + {Name: "password", Type: field.Password, Autocomplete: field.NewPassword, Label: "Password"}, + {Name: "admin", Type: field.Checkbox, Label: "Distillery Administrator"}, }, - FieldTemplate: httpx.PureCSSFieldTemplate, + FieldTemplate: field.PureCSSFieldTemplate, RenderTemplate: userCreateTemplate, RenderTemplateContext: admin.Dependencies.Custom.RenderContext, Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) { - cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == httpx.CheckboxChecked + cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked if cu.User == "" { return cu, errCreateInvalidUsername diff --git a/pkg/httpx/field/autocomplete.go b/pkg/httpx/field/autocomplete.go new file mode 100644 index 0000000..fcb05d7 --- /dev/null +++ b/pkg/httpx/field/autocomplete.go @@ -0,0 +1,63 @@ +package field + +// Autocomplete represents different autocomplete options +type Autocomplete string + +const ( + Off Autocomplete = "off" + On Autocomplete = "on" + Name Autocomplete = "name" + HonorificPrefix Autocomplete = "honorific-prefix" + GivenName Autocomplete = "given-name" + AdditionalName Autocomplete = "additional-name" + FamilyName Autocomplete = "family-name" + HonorificSuffix Autocomplete = "honorific-suffix" + Nickname Autocomplete = "nickname" + Email_ Autocomplete = "email" + Username Autocomplete = "username" + NewPassword Autocomplete = "new-password" + CurrentPassword Autocomplete = "current-password" + OneTimeCode Autocomplete = "one-time-code" + OrganizationTitle Autocomplete = "organization-title" + Organization Autocomplete = "organization" + AddressLine1 Autocomplete = "address-line1" + AddressLine2 Autocomplete = "address-line2" + AddressLine3 Autocomplete = "address-line3" + StreetAddress Autocomplete = "street-address" + AddressLevel4 Autocomplete = "address-level4" + AddressLevel3 Autocomplete = "address-level3" + AddressLevel2 Autocomplete = "address-level2" + AddressLevel1 Autocomplete = "address-level1" + Country Autocomplete = "country" + CountryName Autocomplete = "country-name" + PostalCode Autocomplete = "postal-code" + CcName Autocomplete = "cc-name" + CcGivenName Autocomplete = "cc-given-name" + CcAdditionalName Autocomplete = "cc-additional-name" + CcFamilyName Autocomplete = "cc-family-name" + CcNumber Autocomplete = "cc-number" + CcExp Autocomplete = "cc-exp" + CcExpMonth Autocomplete = "cc-exp-month" + CcExpYear Autocomplete = "cc-exp-year" + CcCsc Autocomplete = "cc-csc" + CcType Autocomplete = "cc-type" + TransactionCurrency Autocomplete = "transaction-currency" + TransactionAmount Autocomplete = "transaction-amount" + Language Autocomplete = "language" + Bday Autocomplete = "bday" + BdayDay Autocomplete = "bday-day" + BdayMonth Autocomplete = "bday-month" + BdayYear Autocomplete = "bday-year" + Sex Autocomplete = "sex" + Tel_ Autocomplete = "tel" + TelCountryCode Autocomplete = "tel-country-code" + TelNational Autocomplete = "tel-national" + TelAreaCode Autocomplete = "tel-area-code" + TelLocal Autocomplete = "tel-local" + TelLocalPrefix Autocomplete = "tel-local-prefix" + TelLocalSuffix Autocomplete = "tel-local-suffix" + TelExtension Autocomplete = "tel-extension" + Impp Autocomplete = "impp" + Url_ Autocomplete = "url" + Photo Autocomplete = "photo" +) diff --git a/pkg/httpx/field/field.go b/pkg/httpx/field/field.go new file mode 100644 index 0000000..061c7d4 --- /dev/null +++ b/pkg/httpx/field/field.go @@ -0,0 +1,40 @@ +package field + +import ( + "html/template" + "io" +) + +// DefaultFieldTemplate is the default template to render fields. +var DefaultFieldTemplate = template.Must(template.New("").Parse(``)) + +// Field represents a field inside a form. +type Field struct { + Name string // Name is the name of the field + Type InputType // Type is the type of the field. It corresponds to the "name" attribute in html. + + Placeholder string // Value for the "placeholder" attribute + Label string // (External) Label for the field. Not used by the default template. + + Autocomplete Autocomplete + + EmptyOnError bool // indicates if the field should be reset on error +} + +// fieldContext is passed to the template context +type fieldContext struct { + Field + Value string +} + +func (field Field) WriteTo(w io.Writer, template *template.Template, value string) { + if template == nil { + template = DefaultFieldTemplate + } + template.Execute(w, fieldContext{Field: field, Value: value}) +} + +// CheckboxChecked is the default value of a checked checkbox +const CheckboxChecked = "on" diff --git a/pkg/httpx/field/type.go b/pkg/httpx/field/type.go new file mode 100644 index 0000000..ca2347f --- /dev/null +++ b/pkg/httpx/field/type.go @@ -0,0 +1,30 @@ +package field + +// InputType represents the type of input +type InputType string + +const ( + Button InputType = "button" + Checkbox InputType = "checkbox" + Color InputType = "color" + Date InputType = "date" + DatetimeLocal InputType = "datetime-local" + Email InputType = "email" + File InputType = "file" + Hidden InputType = "hidden" + Image InputType = "image" + Month InputType = "month" + Number InputType = "number" + Password InputType = "password" + Radio InputType = "radio" + Range InputType = "range" + Reset InputType = "reset" + Search InputType = "search" + Submit InputType = "submit" + Tel InputType = "tel" + Text InputType = "text" + Time InputType = "time" + Url InputType = "url" + Week InputType = "week" + Datetime InputType = "datetime" +) diff --git a/pkg/httpx/form.go b/pkg/httpx/form.go index dc129a4..763fbbe 100644 --- a/pkg/httpx/form.go +++ b/pkg/httpx/form.go @@ -2,23 +2,18 @@ package httpx import ( "html/template" - "io" "net/http" "strings" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx/field" "github.com/gorilla/csrf" ) -// DefaultFieldTemplate is the default template to render fields. -var DefaultFieldTemplate = template.Must(template.New("").Parse(``)) -var PureCSSFieldTemplate = template.Must(template.New("").Parse(` -
`)) - // Form provides a form that a user can submit via a http POST method call. // It implements [http.Handler]. type Form[D any] struct { // Fields are the fields this form consists of. - Fields []Field + Fields []field.Field // FieldTemplate is an optional template to be executed for each field. // FieldTemplate may be nil; in which case [DefaultFieldTemplate] is used. @@ -178,39 +173,3 @@ func (form *Form[D]) renderSuccess(data D, values map[string]string, w http.Resp } form.renderForm(err, values, w, r) } - -// Field represents a field inside a form. -type Field struct { - Name string // Name is the name of the field - Type InputType // Type is the type of the field. It corresponds to the "name" attribute in html. - - Placeholder string // Value for the "placeholder" attribute - Label string // (External) Label for the field. Not used by the default template. - - EmptyOnError bool // indicates if the field should be reset on error -} - -// fieldContext is passed to the template context -type fieldContext struct { - Field - Value string -} - -func (field Field) WriteTo(w io.Writer, template *template.Template, value string) { - if template == nil { - template = DefaultFieldTemplate - } - template.Execute(w, fieldContext{Field: field, Value: value}) -} - -// InputType represents the type of input -type InputType string - -const ( - TextField InputType = "text" - PasswordField InputType = "password" - CheckboxField InputType = "checkbox" -) - -// CheckboxChecked is the default value of a checked checkbox -const CheckboxChecked = "on"