From 185cc4dc60b4b55def32f2d4632b18ad23c95b59 Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Fri, 22 Feb 2019 00:04:57 +0100 Subject: [PATCH 1/4] Easy form generation. TODO: parse forms --- res/template/account/user_password.html | 83 +++++++++++++++++++ res/template/fragments/form.html | 102 ++++++++++++++++++++++++ webcontroller/forms/form.go | 56 +++++++++++++ webcontroller/template_data.go | 10 ++- webcontroller/user_account.go | 37 +++++++++ webcontroller/web_controller.go | 3 + 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 res/template/account/user_password.html create mode 100644 res/template/fragments/form.html create mode 100644 webcontroller/forms/form.go diff --git a/res/template/account/user_password.html b/res/template/account/user_password.html new file mode 100644 index 0000000..f1a9163 --- /dev/null +++ b/res/template/account/user_password.html @@ -0,0 +1,83 @@ +{{define "user_password"}} + + + {{template "meta_tags" "Updating Password"}} + {{template "user_style"}} + + + +
+ {{template "menu" .}} + +

Update Password

+
+
+ + + + + + + + + + + + + + + + + + +
Old Password
New Password
New password verification
Back +
+
+
+ {{template "footer"}} +
+ + + + +{{end}} diff --git a/res/template/fragments/form.html b/res/template/fragments/form.html new file mode 100644 index 0000000..8a662d9 --- /dev/null +++ b/res/template/fragments/form.html @@ -0,0 +1,102 @@ +{{define "form"}} +

{{.Title}}

+ {{.PreFormHTML}} +

+ {{if eq .Submit true}} + {{if eq .SubmitSuccess true}} +
+ {{index .SubmitMessages 0}} +
+ {{else}} +
+ Something went wrong, please correct these errors before continuing:
+ +
+ {{end}} + {{end}} + +
+ + {{range $index, $field := .Fields}} + + + + {{if ne $field.Description ""}} + + + + {{end}} + {{if eq $field.Separator true}} + + {{end}} + + {{end}} + + {{if eq .BackLink ""}} + + {{else}} + + + {{end}} + +
{{$field.Label}} + {{if eq $field.Type "text"}} + + {{else if eq $field.Type "username"}} + + {{else if eq $field.Type "email"}} + + {{else if eq $field.Type "current-password"}} + + {{else if eq $field.Type "new-password"}} + + {{else if eq $field.Type "captcha"}} + +
+ {{end}} +
{{$field.Description}}

+ {{if eq .SubmitRed true}} + + {{else}} + + {{end}} + + Back + + {{if eq .SubmitRed true}} + + {{else}} + + {{end}} +
+
+
+ {{.PostFormHTML}} +
+{{end}} +{{define "form_page"}} + + + + {{template "meta_tags" .Title}} + {{template "user_style" .}} + + + + +
+ {{template "menu" .}} + + {{template "form" .Form}} + + {{template "footer"}} +
+ + {{template "analytics"}} + + +{{end}} diff --git a/webcontroller/forms/form.go b/webcontroller/forms/form.go new file mode 100644 index 0000000..50867d4 --- /dev/null +++ b/webcontroller/forms/form.go @@ -0,0 +1,56 @@ +package forms + +import ( + "html/template" +) + +// Form is a form which can be rendered in HTML and submitted +type Form struct { + Title string // Shown in a large font above the form + PreFormHTML template.HTML // Content to be rendered above the form + + Fields []Field + + BackLink string // Empty for no back link + SubmitLabel string // Label for the submit button + SubmitRed bool // If the submit button should be red or green + + PostFormHTML template.HTML // Content to be rendered below the form + + // Fields to render if the form has been submitted once + Submit bool // If the form has been submitted + SubmitSuccess bool // If the submission was a success + SubmitMessages []string // Messages telling the user the results +} + +// Field is a single input field in a form +type Field struct { + // Used for reading the data. Entered data is POSTed back to the same URL with this name + Name string + // Is entered in the input field by default + DefaultValue string + // Text next to the input field + Label string + // Text below the input field + Description string + // Separates fields with a horizontal rule + Separator bool + + Type FieldType + + // Only used when Type = `captcha` + CaptchaSiteKey string +} + +// FieldType defines the type a form field has and how it should be rendered +type FieldType string + +// Fields which can be in a form +const ( + FieldTypeText FieldType = "text" + FieldTypeUsername FieldType = "username" + FieldTypeEmail FieldType = "email" + FieldTypeOldPassword FieldType = "current-password" + FieldTypeNewPassword FieldType = "new-password" + FieldTypeCaptcha FieldType = "captcha" +) diff --git a/webcontroller/template_data.go b/webcontroller/template_data.go index 8749db7..d89de2f 100644 --- a/webcontroller/template_data.go +++ b/webcontroller/template_data.go @@ -7,6 +7,7 @@ import ( "time" "fornaxian.com/pixeldrain-web/pixelapi" + "fornaxian.com/pixeldrain-web/webcontroller/forms" "github.com/Fornaxian/log" ) @@ -19,12 +20,15 @@ type TemplateData struct { APIEndpoint template.URL PixelAPI *pixelapi.PixelAPI - Other interface{} - URLQuery url.Values - // Only used on file viewer page Title string OGData OGData + + Other interface{} + URLQuery url.Values + + // Only used for pages containing forms + Form forms.Form } func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request) *TemplateData { diff --git a/webcontroller/user_account.go b/webcontroller/user_account.go index f83b4eb..df2fd96 100644 --- a/webcontroller/user_account.go +++ b/webcontroller/user_account.go @@ -1,9 +1,11 @@ package webcontroller import ( + "html/template" "net/http" "fornaxian.com/pixeldrain-web/pixelapi" + "fornaxian.com/pixeldrain-web/webcontroller/forms" "github.com/Fornaxian/log" "github.com/julienschmidt/httprouter" ) @@ -54,3 +56,38 @@ func (wc *WebController) serveLogout( http.Redirect(w, r, "/", http.StatusSeeOther) } + +func (wc *WebController) formPassword( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, +) { + td := wc.newTemplateData(w, r) + td.Form = forms.Form{ + Title: "Test Form", + PreFormHTML: template.HTML("preform"), + Fields: []forms.Field{ + forms.Field{ + Name: "field_1", + DefaultValue: "def val 1", + Label: "Field 1", + Description: "Description of field one", + Separator: false, + Type: forms.FieldTypeUsername, + }, + }, + BackLink: "/", + SubmitLabel: "ayy lmao send", + SubmitRed: false, + PostFormHTML: template.HTML("postform"), + Submit: true, + SubmitSuccess: true, + SubmitMessages: []string{"yay success"}, + } + + err := wc.templates.Get().ExecuteTemplate(w, "form_page", td) + if err != nil { + log.Error("Error executing template '%s': %s", "register", err) + } + +} diff --git a/webcontroller/web_controller.go b/webcontroller/web_controller.go index e55469f..ccf278e 100644 --- a/webcontroller/web_controller.go +++ b/webcontroller/web_controller.go @@ -67,6 +67,9 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon r.GET(p+"/user/files" /* */, wc.serveTemplate("user_files", true)) r.GET(p+"/user/lists" /* */, wc.serveTemplate("user_lists", true)) r.GET(p+"/user/filemanager" /**/, wc.serveTemplate("file_manager", true)) + r.GET(p+"/user/password" /* */, wc.serveTemplate("user_password", true)) + + r.GET(p+"/testform", wc.formPassword) r.NotFound = http.HandlerFunc(wc.serveNotFound) From b78a9d9e517ad7bd02db2b951ac0eec50522e783 Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Mon, 25 Feb 2019 22:53:09 +0100 Subject: [PATCH 2/4] Convert registration and rassword reset forms to new form system --- pixelapi/pixelapi.go | 35 +++-- pixelapi/user.go | 63 +++++++-- res/template/account/user_home.html | 8 ++ res/template/account/user_password.html | 83 ----------- res/template/account/user_settings.html | 6 +- res/template/admin.html | 21 +++ res/template/fragments/form.html | 24 ++-- webcontroller/forms/form.go | 77 ++++++++-- webcontroller/user_account.go | 181 ++++++++++++++++++++---- webcontroller/web_controller.go | 81 +++++++++-- 10 files changed, 405 insertions(+), 174 deletions(-) delete mode 100644 res/template/account/user_password.html create mode 100644 res/template/admin.html diff --git a/pixelapi/pixelapi.go b/pixelapi/pixelapi.go index 4874b16..ab2ca91 100644 --- a/pixelapi/pixelapi.go +++ b/pixelapi/pixelapi.go @@ -44,7 +44,8 @@ func (e Error) Error() string { return e.Value } // SuccessResponse is a generic response the API returns when the action was // successful and there is nothing interesting to report type SuccessResponse struct { - Success bool `json:"success"` + Success bool `json:"success"` + Message string `json:"message"` } func (p *PixelAPI) jsonRequest(method, url string, target interface{}) error { @@ -72,7 +73,7 @@ func (p *PixelAPI) jsonRequest(method, url string, target interface{}) error { } defer resp.Body.Close() - return parseJSONResponse(resp, target) + return parseJSONResponse(resp, target, true) } func (p *PixelAPI) getString(url string) (string, error) { @@ -84,8 +85,6 @@ func (p *PixelAPI) getString(url string) (string, error) { req.SetBasicAuth("", p.apiKey) } - client := &http.Client{} - resp, err := client.Do(req) if err != nil { return "", err @@ -115,10 +114,16 @@ func (p *PixelAPI) getRaw(url string) (io.ReadCloser, error) { return resp.Body, err } -func (p *PixelAPI) postForm(url string, vals url.Values, target interface{}) error { - req, err := http.NewRequest("POST", url, strings.NewReader(vals.Encode())) +func (p *PixelAPI) form( + method string, + url string, + vals url.Values, + target interface{}, + catchErrors bool, +) error { + req, err := http.NewRequest(method, url, strings.NewReader(vals.Encode())) if err != nil { - return &Error{ + return Error{ ReqError: true, Success: false, Value: err.Error(), @@ -129,9 +134,11 @@ func (p *PixelAPI) postForm(url string, vals url.Values, target interface{}) err req.SetBasicAuth("", p.apiKey) } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) if err != nil { - return &Error{ + return Error{ ReqError: true, Success: false, Value: err.Error(), @@ -140,22 +147,22 @@ func (p *PixelAPI) postForm(url string, vals url.Values, target interface{}) err } defer resp.Body.Close() - return parseJSONResponse(resp, target) + return parseJSONResponse(resp, target, catchErrors) } -func parseJSONResponse(resp *http.Response, target interface{}) error { +func parseJSONResponse(resp *http.Response, target interface{}, catchErrors bool) error { var jdec = json.NewDecoder(resp.Body) var err error // Test for client side and server side errors - if resp.StatusCode >= 400 { - var errResp = &Error{ + if catchErrors && resp.StatusCode >= 400 { + var errResp = Error{ ReqError: false, } err = jdec.Decode(&errResp) if err != nil { log.Error("Can't decode this: %v", err) - return &Error{ + return Error{ ReqError: true, Success: false, Value: err.Error(), @@ -169,7 +176,7 @@ func parseJSONResponse(resp *http.Response, target interface{}) error { if err != nil { r, _ := ioutil.ReadAll(resp.Body) log.Error("Can't decode this: %v. %s", err, r) - return &Error{ + return Error{ ReqError: true, Success: false, Value: err.Error(), diff --git a/pixelapi/user.go b/pixelapi/user.go index d08e23a..7df6bd4 100644 --- a/pixelapi/user.go +++ b/pixelapi/user.go @@ -5,22 +5,24 @@ import ( "net/url" ) +// Registration is the response to the UserRegister API. The register API can +// return multiple errors, which will be stored in the Errors array. Check for +// len(Errors) == 0 to see if an error occurred type Registration struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Errors []RegistrationError `json:"errors,omitempty"` -} - -type RegistrationError struct { - Code string `json:"error_code"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Errors []Error `json:"errors,omitempty"` } // UserRegister registers a new user on the Pixeldrain server. username and -// password are always required. email is optional, but without it you will -// never be able to reset your password in case you forget it. captcha depends -// on whether reCaptcha is enabled on the Pixeldrain server, this can be checked +// password are always required. email is optional, but without it you will not +// be able to reset your password in case you forget it. captcha depends on +// whether reCaptcha is enabled on the Pixeldrain server, this can be checked // through the GetRecaptcha function. +// +// The register API can return multiple errors, which will be stored in the +// Errors array. Check for len(Errors) == 0 to see if an error occurred. If err +// != nil it means a connection error occurred func (p *PixelAPI) UserRegister(username, email, password, captcha string) (resp *Registration, err error) { resp = &Registration{} var form = url.Values{} @@ -28,13 +30,38 @@ func (p *PixelAPI) UserRegister(username, email, password, captcha string) (resp form.Add("email", email) form.Add("password", password) form.Add("recaptcha_response", captcha) - err = p.postForm(p.apiEndpoint+"/user/register", form, resp) + err = p.form("POST", p.apiEndpoint+"/user/register", form, resp, false) if err != nil { return nil, err } return resp, nil } +// Login is the success response to the `user/login` API +type Login struct { + Success bool `json:"success"` + APIKey string `json:"api_key"` +} + +// UserLogin logs a user in with the provided credentials. The response will +// contain the returned API key. If saveKey is true the API key will also be +// saved in the client and following requests with this client will be +// autenticated +func (p *PixelAPI) UserLogin(username, password string, saveKey bool) (resp *Login, err error) { + resp = &Login{} + var form = url.Values{} + form.Add("username", username) + form.Add("password", password) + err = p.form("POST", p.apiEndpoint+"/user/login", form, resp, true) + if err != nil { + return nil, err + } + if saveKey { + p.apiKey = resp.APIKey + } + return resp, nil +} + // UserInfo contains information about the logged in user type UserInfo struct { Success bool `json:"success"` @@ -97,3 +124,15 @@ func (p *PixelAPI) UserLists(page, limit int) (resp *UserLists, err error) { } return resp, nil } + +func (p *PixelAPI) UserPasswordSet(oldPW, newPW string) (resp *SuccessResponse, err error) { + resp = &SuccessResponse{} + var form = url.Values{} + form.Add("old_password", oldPW) + form.Add("new_password", newPW) + err = p.form("PUT", p.apiEndpoint+"/user/password", form, resp, true) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/res/template/account/user_home.html b/res/template/account/user_home.html index db769a0..e7a7edd 100644 --- a/res/template/account/user_home.html +++ b/res/template/account/user_home.html @@ -11,6 +11,14 @@ {{template "menu" .}}

Welcome home, {{.Username}}!

+ + +

Actions

+ +

Your most recently uploaded files:

{{$files := .PixelAPI.UserFiles 0 18}} diff --git a/res/template/account/user_password.html b/res/template/account/user_password.html deleted file mode 100644 index f1a9163..0000000 --- a/res/template/account/user_password.html +++ /dev/null @@ -1,83 +0,0 @@ -{{define "user_password"}} - - - {{template "meta_tags" "Updating Password"}} - {{template "user_style"}} - - - -
- {{template "menu" .}} - -

Update Password

-
-
- - - - - - - - - - - - - - - - - - -
Old Password
New Password
New password verification
Back -
-
-
- {{template "footer"}} -
- - - - -{{end}} diff --git a/res/template/account/user_settings.html b/res/template/account/user_settings.html index ffb037b..eb7e8e0 100644 --- a/res/template/account/user_settings.html +++ b/res/template/account/user_settings.html @@ -10,8 +10,10 @@
{{template "menu" .}}

User configuration

- - +

What would you like to do?

+ {{template "footer"}}
diff --git a/res/template/admin.html b/res/template/admin.html new file mode 100644 index 0000000..5f818af --- /dev/null +++ b/res/template/admin.html @@ -0,0 +1,21 @@ +{{define "widgets"}} + + + + {{template "meta_tags" "Administrator panel"}} + {{template "user_style" .}} + + + + Header image +
+
+ {{template "menu" .}} + +

System statistics

+ + {{template "footer"}} +
+ + +{{end}} diff --git a/res/template/fragments/form.html b/res/template/fragments/form.html index 8a662d9..854b1f5 100644 --- a/res/template/fragments/form.html +++ b/res/template/fragments/form.html @@ -1,8 +1,7 @@ {{define "form"}}

{{.Title}}

{{.PreFormHTML}} -

- {{if eq .Submit true}} + {{if eq .Submitted true}} {{if eq .SubmitSuccess true}}
{{index .SubmitMessages 0}} @@ -19,7 +18,12 @@ {{end}} {{end}} -
+ + + {{if ne .Username ""}} + + + {{end}} {{range $index, $field := .Fields}} @@ -40,14 +44,16 @@
{{end}} - {{if ne $field.Description ""}} + {{if or (ne $field.Description "") (eq $field.Separator true)}} - + {{end}} - {{if eq $field.Separator true}} - - {{end}} {{end}} @@ -74,9 +80,7 @@
{{$field.Description}} + {{$field.Description}} + {{if eq $field.Separator true}} +
+ {{end}} +

-
{{.PostFormHTML}} -
{{end}} {{define "form_page"}} diff --git a/webcontroller/forms/form.go b/webcontroller/forms/form.go index 50867d4..c3a92d0 100644 --- a/webcontroller/forms/form.go +++ b/webcontroller/forms/form.go @@ -1,11 +1,16 @@ package forms import ( + "fmt" "html/template" + "net/http" ) // Form is a form which can be rendered in HTML and submitted type Form struct { + // Name of the form. When this form is submitted this name will be in the `form` parameter + Name string + Title string // Shown in a large font above the form PreFormHTML template.HTML // Content to be rendered above the form @@ -18,27 +23,39 @@ type Form struct { PostFormHTML template.HTML // Content to be rendered below the form // Fields to render if the form has been submitted once - Submit bool // If the form has been submitted - SubmitSuccess bool // If the submission was a success - SubmitMessages []string // Messages telling the user the results + Submitted bool // If the form has been submitted + SubmitSuccess bool // If the submission was a success + SubmitMessages []template.HTML // Messages telling the user the results + + // Used for letting the browser know which user is logged in + Username string } // Field is a single input field in a form type Field struct { // Used for reading the data. Entered data is POSTed back to the same URL with this name Name string - // Is entered in the input field by default + + // Is entered in the input field by default. If this is empty when running + // Form.ReadInput() it will be set to the value entered by the user DefaultValue string + + // The value entered by the user. Filled in when running Form.ReadInput() + EnteredValue string + // Text next to the input field Label string + // Text below the input field Description string + // Separates fields with a horizontal rule Separator bool Type FieldType - // Only used when Type = `captcha` + // Only used when Type = `captcha`. When using reCaptcha the field name has + // to be `recaptcha_response` CaptchaSiteKey string } @@ -47,10 +64,48 @@ type FieldType string // Fields which can be in a form const ( - FieldTypeText FieldType = "text" - FieldTypeUsername FieldType = "username" - FieldTypeEmail FieldType = "email" - FieldTypeOldPassword FieldType = "current-password" - FieldTypeNewPassword FieldType = "new-password" - FieldTypeCaptcha FieldType = "captcha" + FieldTypeText FieldType = "text" + FieldTypeUsername FieldType = "username" + FieldTypeEmail FieldType = "email" + FieldTypeCurrentPassword FieldType = "current-password" + FieldTypeNewPassword FieldType = "new-password" + FieldTypeCaptcha FieldType = "captcha" ) + +// ReadInput reads the form of a request and fills in the values for each field. +// The return value will be true if this form was submitted and false if the +// form was not submitted +func (f *Form) ReadInput(r *http.Request) (success bool) { + if r.FormValue("form") != f.Name { + f.Submitted = false + return false + } + f.Submitted = true + + var val string + + for i, field := range f.Fields { + val = r.FormValue(field.Name) + field.EnteredValue = val + + if field.DefaultValue == "" { + field.DefaultValue = val + } + f.Fields[i] = field // Update the new values in the array + } + + return true +} + +// FieldVal is a utility function for getting the entered value of a field by +// its name. By using this function you don't have to use nondescriptive array +// indexes to get the values. It panics if the field name is not found in the +// form +func (f *Form) FieldVal(name string) (enteredValue string) { + for _, field := range f.Fields { + if field.Name == name { + return field.EnteredValue + } + } + panic(fmt.Errorf("FieldVal called on unregistered field name '%s'", name)) +} diff --git a/webcontroller/user_account.go b/webcontroller/user_account.go index df2fd96..a76a76b 100644 --- a/webcontroller/user_account.go +++ b/webcontroller/user_account.go @@ -19,8 +19,7 @@ func (wc *WebController) serveRegister( // This only runs on the first request if wc.captchaSiteKey == "" { - var api = pixelapi.New(wc.conf.APIURLInternal, "") - capt, err := api.GetRecaptcha() + capt, err := tpld.PixelAPI.GetRecaptcha() if err != nil { log.Error("Error getting recaptcha key: %s", err) w.WriteHeader(http.StatusInternalServerError) @@ -57,37 +56,161 @@ func (wc *WebController) serveLogout( http.Redirect(w, r, "/", http.StatusSeeOther) } -func (wc *WebController) formPassword( - w http.ResponseWriter, - r *http.Request, - p httprouter.Params, -) { - td := wc.newTemplateData(w, r) - td.Form = forms.Form{ - Title: "Test Form", - PreFormHTML: template.HTML("preform"), +func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f forms.Form) { + // This only runs on the first request + if wc.captchaSiteKey == "" { + capt, err := td.PixelAPI.GetRecaptcha() + if err != nil { + log.Error("Error getting recaptcha key: %s", err) + f.SubmitMessages = []template.HTML{ + "An internal server error had occurred. Registration is " + + "unavailable at the moment. Please return later", + } + return f + } + if capt.SiteKey == "" { + wc.captchaSiteKey = "none" + } else { + wc.captchaSiteKey = capt.SiteKey + } + } + + // Construct the form + td.Title = "Register a new Pixeldrain account" + f = forms.Form{ + Name: "register", + Title: td.Title, Fields: []forms.Field{ - forms.Field{ - Name: "field_1", - DefaultValue: "def val 1", - Label: "Field 1", - Description: "Description of field one", - Separator: false, - Type: forms.FieldTypeUsername, + { + Name: "username", + Label: "Username", + Description: "used for logging into your account", + Separator: true, + Type: forms.FieldTypeUsername, + }, { + Name: "e-mail", + Label: "E-mail address", + Description: "not required. your e-mail address will only be " + + "used for password resets and important account " + + "notifications", + Separator: true, + Type: forms.FieldTypeEmail, + }, { + Name: "password1", + Label: "Password", + Type: forms.FieldTypeNewPassword, + }, { + Name: "password2", + Label: "Password verification", + Description: "you need to enter your password twice so we " + + "can verify that no typing errors were made, which would " + + "prevent you from logging into your new account", + Separator: true, + Type: forms.FieldTypeNewPassword, + }, { + Name: "recaptcha_response", + Label: "Turing test (click the white box)", + Description: "the reCaptcha turing test verifies that you " + + "are not an evil robot that is trying to flood the " + + "website with fake accounts", + Separator: true, + Type: forms.FieldTypeCaptcha, + CaptchaSiteKey: wc.captchaSiteKey, }, }, - BackLink: "/", - SubmitLabel: "ayy lmao send", - SubmitRed: false, - PostFormHTML: template.HTML("postform"), - Submit: true, - SubmitSuccess: true, - SubmitMessages: []string{"yay success"}, + BackLink: "/", + SubmitLabel: "Register", + PostFormHTML: template.HTML("

Welcome to the club!

"), } - err := wc.templates.Get().ExecuteTemplate(w, "form_page", td) - if err != nil { - log.Error("Error executing template '%s': %s", "register", err) - } + if f.ReadInput(r) { + if f.FieldVal("password1") != f.FieldVal("password2") { + f.SubmitMessages = []template.HTML{ + "Password verification failed. Please enter the same " + + "password in both password fields"} + return f + } + resp, err := td.PixelAPI.UserRegister( + f.FieldVal("username"), + f.FieldVal("e-mail"), + f.FieldVal("password1"), + f.FieldVal("recaptcha_response"), + ) + if err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else if len(resp.Errors) != 0 { + // Registration errors occurred + for _, rerr := range resp.Errors { + f.SubmitMessages = append(f.SubmitMessages, template.HTML(rerr.Message)) + } + } else { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{ + `Registration completed! You can now log in ` + + `to your account.
We're glad to have you on ` + + `board, have fun sharing!`} + } + } + return f +} + +func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f forms.Form) { + td.Title = "Change Password" + f = forms.Form{ + Name: "password_change", + Title: td.Title, + Fields: []forms.Field{ + { + Name: "old_password", + Label: "Old Password", + Type: forms.FieldTypeCurrentPassword, + }, { + Name: "new_password1", + Label: "New Password", + Type: forms.FieldTypeNewPassword, + }, { + Name: "new_password2", + Label: "New Password verification", + Type: forms.FieldTypeCurrentPassword, + }, + }, + BackLink: "/user", + SubmitLabel: "Submit", + } + + if f.ReadInput(r) { + if f.FieldVal("new_password1") != f.FieldVal("new_password2") { + f.SubmitMessages = []template.HTML{ + "Password verification failed. Please enter the same " + + "password in both new password fields"} + return f + } + + // Passwords match, send the request and fill in the response in the + // form + _, err := td.PixelAPI.UserPasswordSet( + f.FieldVal("old_password"), + f.FieldVal("new_password1"), + ) + if err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{"Success! Your password has been updated"} + } + } + return f } diff --git a/webcontroller/web_controller.go b/webcontroller/web_controller.go index ccf278e..693ba72 100644 --- a/webcontroller/web_controller.go +++ b/webcontroller/web_controller.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "fornaxian.com/pixeldrain-web/init/conf" + "fornaxian.com/pixeldrain-web/webcontroller/forms" "github.com/Fornaxian/log" "github.com/julienschmidt/httprouter" ) @@ -47,19 +48,21 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon }) // General navigation - r.GET(p+"/" /* */, wc.serveTemplate("home", false)) - r.GET(p+"/favicon.ico" /* */, wc.serveFile("/favicon.ico")) - r.GET(p+"/api" /* */, wc.serveTemplate("apidoc", false)) - r.GET(p+"/history" /* */, wc.serveTemplate("history_cookies", false)) - r.GET(p+"/u/:id" /* */, wc.serveFileViewer) - r.GET(p+"/u/:id/preview" /* */, wc.serveFilePreview) - r.GET(p+"/l/:id" /* */, wc.serveListViewer) - r.GET(p+"/t" /* */, wc.serveTemplate("paste", false)) - r.GET(p+"/donation" /* */, wc.serveTemplate("donation", false)) - r.GET(p+"/widgets" /* */, wc.serveTemplate("widgets", false)) + r.GET(p+"/" /* */, wc.serveTemplate("home", false)) + r.GET(p+"/favicon.ico" /* */, wc.serveFile("/favicon.ico")) + r.GET(p+"/api" /* */, wc.serveTemplate("apidoc", false)) + r.GET(p+"/history" /* */, wc.serveTemplate("history_cookies", false)) + r.GET(p+"/u/:id" /* */, wc.serveFileViewer) + r.GET(p+"/u/:id/preview" /**/, wc.serveFilePreview) + r.GET(p+"/l/:id" /* */, wc.serveListViewer) + r.GET(p+"/t" /* */, wc.serveTemplate("paste", false)) + r.GET(p+"/donation" /* */, wc.serveTemplate("donation", false)) + r.GET(p+"/widgets" /* */, wc.serveTemplate("widgets", false)) // User account pages - r.GET(p+"/register" /* */, wc.serveRegister) + r.GET(p+"/register_old" /* */, wc.serveRegister) + r.GET(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) + r.POST(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) r.GET(p+"/login" /* */, wc.serveTemplate("login", false)) r.GET(p+"/logout" /* */, wc.serveTemplate("logout", true)) r.POST(p+"/logout" /* */, wc.serveLogout) @@ -67,9 +70,11 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon r.GET(p+"/user/files" /* */, wc.serveTemplate("user_files", true)) r.GET(p+"/user/lists" /* */, wc.serveTemplate("user_lists", true)) r.GET(p+"/user/filemanager" /**/, wc.serveTemplate("file_manager", true)) - r.GET(p+"/user/password" /* */, wc.serveTemplate("user_password", true)) - r.GET(p+"/testform", wc.formPassword) + // User account settings + r.GET(p+"/user/settings" /* */, wc.serveTemplate("user_settings", true)) + r.GET(p+"/user/change_password" /* */, wc.serveForm(wc.passwordForm, true)) + r.POST(p+"/user/change_password" /**/, wc.serveForm(wc.passwordForm, true)) r.NotFound = http.HandlerFunc(wc.serveNotFound) @@ -107,6 +112,56 @@ func (wc *WebController) serveFile(path string) httprouter.Handle { } } +func (wc *WebController) serveForm( + handler func(*TemplateData, *http.Request) forms.Form, + requireAuth bool, +) httprouter.Handle { + return func( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, + ) { + var td = wc.newTemplateData(w, r) + if requireAuth && !td.Authenticated { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + // The handler retuns the form which will be rendered + td.Form = handler(td, r) + + td.Form.Username = td.Username + + // Remove the recaptcha field if captcha is disabled + if wc.captchaSiteKey == "none" { + for i, field := range td.Form.Fields { + if field.Type == forms.FieldTypeCaptcha { + td.Form.Fields = append( + td.Form.Fields[:i], + td.Form.Fields[i+1:]..., + ) + } + } + } + + // Clear the entered values if the request was successful + if td.Form.SubmitSuccess { + w.WriteHeader(http.StatusOK) + for i, field := range td.Form.Fields { + field.EnteredValue = "" + td.Form.Fields[i] = field + } + } else { + w.WriteHeader(http.StatusBadRequest) + } + + err := wc.templates.Get().ExecuteTemplate(w, "form_page", td) + if err != nil { + log.Error("Error executing form page: %s", err) + } + } +} + func (wc *WebController) serveNotFound(w http.ResponseWriter, r *http.Request) { log.Debug("Not Found: %s", r.URL) w.WriteHeader(http.StatusNotFound) From c06644662c2991c98f36e12239d6f2f23dfeb93b Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Tue, 26 Mar 2019 20:53:19 +0100 Subject: [PATCH 3/4] Add bandwidth counter to file viewer --- pixelapi/file.go | 1 + pixelapi/list.go | 15 ++++++++------- res/static/script/ListNavigator.js | 6 ------ res/static/script/Toolbar.js | 11 ++++++++--- res/static/script/Viewer.js | 1 - res/static/style/viewer.css | 7 +++++++ res/template/file_viewer.html | 5 ++++- res/template/fragments/api/file.html | 1 + res/template/fragments/api/list.html | 9 ++++++--- webcontroller/file_viewer.go | 21 +++++++++++---------- 10 files changed, 46 insertions(+), 31 deletions(-) diff --git a/pixelapi/file.go b/pixelapi/file.go index 4e27130..2fd7343 100644 --- a/pixelapi/file.go +++ b/pixelapi/file.go @@ -18,6 +18,7 @@ type FileInfo struct { Name string `json:"name"` Size uint64 `json:"size"` Views int64 `json:"views"` + BandwidthUsed uint64 `json:"bandwidth_used"` DateUpload time.Time `json:"date_upload"` DateLastView time.Time `json:"date_last_view"` MimeType string `json:"mime_type"` diff --git a/pixelapi/list.go b/pixelapi/list.go index 4f2af11..3bb40bb 100644 --- a/pixelapi/list.go +++ b/pixelapi/list.go @@ -19,13 +19,14 @@ type List struct { // ListFile information object from the pixeldrain API type ListFile struct { - ID string `json:"id"` - DetailHREF string `json:"detail_href"` - Name string `json:"name"` - Description string `json:"description"` - DateCreated time.Time `json:"date_created"` - DateLastView time.Time `json:"date_last_view"` - Views int64 `json:"views"` + ID string `json:"id"` + DetailHREF string `json:"detail_href"` + Name string `json:"name"` + Description string `json:"description"` + DateCreated time.Time `json:"date_created"` + DateLastView time.Time `json:"date_last_view"` + Views int64 `json:"views"` + BandwidthUsed uint64 `json:"bandwidth_used"` } // GetList get a List from the pixeldrain API. Errors will be available through diff --git a/res/static/script/ListNavigator.js b/res/static/script/ListNavigator.js index ee02360..583ee9d 100644 --- a/res/static/script/ListNavigator.js +++ b/res/static/script/ListNavigator.js @@ -209,12 +209,6 @@ var ListNavigator = { document.getElementById("button-expand-toolbar").style.top = navHeight+"px"; document.getElementById("sharebar").style.top = navHeight+"px"; document.getElementById("info_popup").style.top = (navHeight+20)+"px"; - // $("#listNavigator").animate( {top: 0}, {"duration": 1500, "queue": false}); - // $("#filepreview").animate( {top: navHeight},{"duration": 1500, "queue": false}); - // $("#toolbar").animate( {top: navHeight},{"duration": 1500, "queue": false}); - // $("#button-expand-toolbar").animate({top: navHeight},{"duration": 1500, "queue": false}); - // $("#sharebar").animate( {top: navHeight},{"duration": 1500, "queue": false}); - // $("#info_popup").css("top", "120px"); }, 200); } }; diff --git a/res/static/script/Toolbar.js b/res/static/script/Toolbar.js index ed2f71d..e6c9dda 100644 --- a/res/static/script/Toolbar.js +++ b/res/static/script/Toolbar.js @@ -55,8 +55,9 @@ var Toolbar = { document.getElementById("btnCopy").classList.remove("button_highlight") }, 60000); }, - setViews: function(amount){ - document.getElementById("views").innerText = "Views: "+amount; + setStats: function(views, downloads){ + document.getElementById("stat_views").innerText = views + document.getElementById("stat_downloads").innerText = Math.round(downloads*10)/10; } }; @@ -127,12 +128,14 @@ var DetailsWindow = { + "Name" + escapeHTML(data.name) + "" + "Url/u/" + data.id + "" + "Mime Type" + escapeHTML(data.mime_type) + "" - + "IS" + data.id + "" + + "ID" + data.id + "" + "Size" + data.size + "" + + "Bandwidth" + data.bandwidth_used + "" + "Upload Date" + data.date_upload + "" + "Description" + escapeHTML(file.description) + "" + "" ); + Toolbar.setStats(data.views, data.bandwidth_used/data.size); } }); } else { @@ -142,9 +145,11 @@ var DetailsWindow = { + "Mime Type" + escapeHTML(file.mime_type) + "" + "ID" + file.id + "" + "Size" + file.size + "" + + "Bandwidth" + file.bandwidth_used + "" + "Upload Date" + file.date_upload + "" + "" ); + Toolbar.setStats(file.views, file.bandwidth_used/file.size); } } }; diff --git a/res/static/script/Viewer.js b/res/static/script/Viewer.js index b77c201..10f7444 100644 --- a/res/static/script/Viewer.js +++ b/res/static/script/Viewer.js @@ -40,7 +40,6 @@ var Viewer = { }); DetailsWindow.setDetails(file); - Toolbar.setViews(file.views); } }; diff --git a/res/static/style/viewer.css b/res/static/style/viewer.css index af30858..ba2fba1 100644 --- a/res/static/style/viewer.css +++ b/res/static/style/viewer.css @@ -148,6 +148,13 @@ body{ vertical-align: 6px; } +.toolbar_label { + text-align: left; + padding-left: 10px; + font-size: 0.8em; + line-height: 0.7em; +} + #sponsors{ position: relative; height: 600px; diff --git a/res/template/file_viewer.html b/res/template/file_viewer.html index 3a6b162..99e1ea1 100644 --- a/res/template/file_viewer.html +++ b/res/template/file_viewer.html @@ -45,7 +45,10 @@
-
Views: No
+
Views
+
N/A
+
Downloads
+
N/A
Back to the Home page diff --git a/res/template/fragments/api/file.html b/res/template/fragments/api/file.html index fb01f68..44435fd 100644 --- a/res/template/fragments/api/file.html +++ b/res/template/fragments/api/file.html @@ -158,6 +158,7 @@ "date_last_view": 1485894987, // Timestamp "size": 5694837, // Bytes "views" 1234, // Amount of unique file views + "bandwidth_used": 1234567890, // Bytes "mime_type" "image/png", "description": "File description", "mime_image": "http://pixeldra.in/res/img/mime/image-png.png", // Image associated with the mime type diff --git a/res/template/fragments/api/list.html b/res/template/fragments/api/list.html index 4156599..ef79f3a 100644 --- a/res/template/fragments/api/list.html +++ b/res/template/fragments/api/list.html @@ -130,7 +130,8 @@ "description": "", "date_created": 1513033304, "date_last_view": 1513033304, - "views": 1 + "views": 1, + "bandwidth_used": 1234567890 }, { "detail_href": "/file/RKwgZb/info", @@ -139,7 +140,8 @@ "description": "", "date_created": 1513033304, "date_last_view": 1513033304, - "views": 2 + "views": 2, + "bandwidth_used": 1234567890 }, { "detail_href": "/file/DRaL_e/info", @@ -148,7 +150,8 @@ "description": "", "date_created": 1513033304, "date_last_view": 1513033304, - "views": 3 + "views": 3, + "bandwidth_used": 1234567890 } ] } diff --git a/webcontroller/file_viewer.go b/webcontroller/file_viewer.go index e4fdd8b..887816e 100644 --- a/webcontroller/file_viewer.go +++ b/webcontroller/file_viewer.go @@ -82,16 +82,17 @@ func (wc *WebController) serveFileViewerDemo(w http.ResponseWriter, r *http.Requ templateData.Other = viewerData{ Type: "file", APIResponse: map[string]interface{}{ - "id": "demo", - "name": "Demo file", - "date_upload": "2017-01-01 12:34:56", - "date_lastview": "2017-01-01 12:34:56", - "size": 123456789, - "views": 1, - "mime_type": "text/demo", - "description": "A file to demonstrate the viewer page", - "mime_image": "/res/img/mime/text.png", - "thumbnail": "/res/img/mime/text.png", + "id": "demo", + "name": "Demo file", + "date_upload": "2017-01-01 12:34:56", + "date_lastview": "2017-01-01 12:34:56", + "size": 123456789, + "views": 1, + "bandwidth_used": 123456789, + "mime_type": "text/demo", + "description": "A file to demonstrate the viewer page", + "mime_image": "/res/img/mime/text.png", + "thumbnail": "/res/img/mime/text.png", }, } err := wc.templates.Get().ExecuteTemplate(w, "file_viewer", templateData) From 86921e9b3b59e582cbd9bb09ce46e198c7fac54b Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Sun, 31 Mar 2019 22:33:22 +0200 Subject: [PATCH 4/4] Move login screen to new forms framework --- res/static/style/layout.css | 2 +- res/template/account/login.html | 69 ----------------------------- res/template/account/logout.html | 12 ++--- res/template/account/user_home.html | 3 +- webcontroller/forms/form.go | 30 ++++++++++--- webcontroller/user_account.go | 56 ++++++++++++++++++++++- webcontroller/web_controller.go | 16 ++++++- 7 files changed, 99 insertions(+), 89 deletions(-) delete mode 100644 res/template/account/login.html diff --git a/res/static/style/layout.css b/res/static/style/layout.css index 236b09f..3590028 100644 --- a/res/static/style/layout.css +++ b/res/static/style/layout.css @@ -103,7 +103,7 @@ body{ font-family: "Lato Thin", sans-serif; font-weight: bold; font-size: 1.8em; - transition: box-shadow 2s; + transition: box-shadow 5s; } .navigation a:hover { background: linear-gradient(var(--highlight_color), var(--highlight_color_dark)); diff --git a/res/template/account/login.html b/res/template/account/login.html deleted file mode 100644 index 4007995..0000000 --- a/res/template/account/login.html +++ /dev/null @@ -1,69 +0,0 @@ -{{define "login"}} - - - - {{template "meta_tags" "Login"}} - {{template "user_style" .}} - - - - - - - {{template "analytics"}} - - -{{end}} diff --git a/res/template/account/logout.html b/res/template/account/logout.html index 6aa9a48..e05f4f4 100644 --- a/res/template/account/logout.html +++ b/res/template/account/logout.html @@ -8,18 +8,18 @@
{{template "menu" .}} -

Please confirm that you want to log out of your Pixeldrain account

+

Please confirm that you want to log out of your pixeldrain account


Why do I need to confirm my logout?

- We need you to confirm your action here so we can be sure that - you really requested a logout. If we didn't do this, anyone (or - any website) would be able to send you to this page and you - would automatically get logged out of Pixeldrain, which would be - very annoying. + We need you to confirm your action so we can be sure that you + really requested a logout. If we didn't do this, anyone (or any + website) would be able to send you to this page and you would + automatically get logged out of pixeldrain, which would be very + annoying.

To prevent this from happening we're verifying that you actually diff --git a/res/template/account/user_home.html b/res/template/account/user_home.html index e7a7edd..9771c01 100644 --- a/res/template/account/user_home.html +++ b/res/template/account/user_home.html @@ -12,11 +12,10 @@

Welcome home, {{.Username}}!

-

Actions

Your most recently uploaded files:

diff --git a/webcontroller/forms/form.go b/webcontroller/forms/form.go index c3a92d0..3f23e41 100644 --- a/webcontroller/forms/form.go +++ b/webcontroller/forms/form.go @@ -29,6 +29,9 @@ type Form struct { // Used for letting the browser know which user is logged in Username string + + // Actions to perform when the form is rendered + Extra ExtraActions } // Field is a single input field in a form @@ -54,11 +57,22 @@ type Field struct { Type FieldType - // Only used when Type = `captcha`. When using reCaptcha the field name has - // to be `recaptcha_response` + // Only used when Type == FieldTypeCaptcha CaptchaSiteKey string } +// ExtraActions contains extra actions to performs when rendering the form +type ExtraActions struct { + // Redirects the browser to a different URL with a HTTP 303: See Other + // status. This is useful for redirecting the user to a different page if + // the form submission was successful + RedirectTo string + + // A cookie to install in the browser when the form is rendered. Useful for + // setting / destroying user sessions or configurations + SetCookie *http.Cookie +} + // FieldType defines the type a form field has and how it should be rendered type FieldType string @@ -82,15 +96,17 @@ func (f *Form) ReadInput(r *http.Request) (success bool) { } f.Submitted = true - var val string - for i, field := range f.Fields { - val = r.FormValue(field.Name) - field.EnteredValue = val + field.EnteredValue = r.FormValue(field.Name) if field.DefaultValue == "" { - field.DefaultValue = val + field.DefaultValue = field.EnteredValue } + + if field.Type == FieldTypeCaptcha && field.EnteredValue == "" { + field.EnteredValue = r.FormValue("g-recaptcha-response") + } + f.Fields[i] = field // Update the new values in the array } diff --git a/webcontroller/user_account.go b/webcontroller/user_account.go index 24b94e4..e0f0c1b 100644 --- a/webcontroller/user_account.go +++ b/webcontroller/user_account.go @@ -3,6 +3,7 @@ package webcontroller import ( "html/template" "net/http" + "time" "fornaxian.com/pixeldrain-web/pixelapi" "fornaxian.com/pixeldrain-web/webcontroller/forms" @@ -99,7 +100,7 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form "website with fake accounts", Separator: true, Type: forms.FieldTypeCaptcha, - CaptchaSiteKey: wc.captchaSiteKey, + CaptchaSiteKey: wc.captchaKey(), }, }, BackLink: "/", @@ -114,7 +115,7 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form "password in both password fields"} return f } - + log.Debug("capt: %s", f.FieldVal("recaptcha_response")) resp, err := td.PixelAPI.UserRegister( f.FieldVal("username"), f.FieldVal("e-mail"), @@ -145,6 +146,57 @@ func (wc *WebController) registerForm(td *TemplateData, r *http.Request) (f form return f } +func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f forms.Form) { + td.Title = "Login" + f = forms.Form{ + Name: "login", + Title: "Log in to your pixeldrain account", + Fields: []forms.Field{ + { + Name: "username", + Label: "Username / e-mail", + Type: forms.FieldTypeUsername, + }, { + Name: "password", + Label: "Password", + Type: forms.FieldTypeCurrentPassword, + }, + }, + BackLink: "/", + SubmitLabel: "Login", + PostFormHTML: template.HTML( + `
If you don't have a pixeldrain account yet, you can ` + + `register here. No e-mail address is ` + + `required.
`, + ), + } + + if f.ReadInput(r) { + loginResp, err := td.PixelAPI.UserLogin(f.FieldVal("username"), f.FieldVal("password"), false) + if err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else { + log.Debug("key %s", loginResp.APIKey) + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{"Success!"} + f.Extra.SetCookie = &http.Cookie{ + Name: "pd_auth_key", + Value: loginResp.APIKey, + Path: "/", + Expires: time.Now().AddDate(50, 0, 0), + } + f.Extra.RedirectTo = "/user" + } + } + return f +} + func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f forms.Form) { td.Title = "Change Password" f = forms.Form{ diff --git a/webcontroller/web_controller.go b/webcontroller/web_controller.go index 3eb69c9..303396f 100644 --- a/webcontroller/web_controller.go +++ b/webcontroller/web_controller.go @@ -64,7 +64,9 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon r.GET(p+"/register_old" /* */, wc.serveRegister) r.GET(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) r.POST(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) - r.GET(p+"/login" /* */, wc.serveTemplate("login", false)) + r.GET(p+"/login" /* */, wc.serveForm(wc.loginForm, false)) + r.POST(p+"/login" /* */, wc.serveForm(wc.loginForm, false)) + // r.GET(p+"/login" /* */, wc.serveTemplate("login", false)) r.GET(p+"/logout" /* */, wc.serveTemplate("logout", true)) r.POST(p+"/logout" /* */, wc.serveLogout) r.GET(p+"/user" /* */, wc.serveTemplate("user_home", true)) @@ -133,8 +135,18 @@ func (wc *WebController) serveForm( td.Form.Username = td.Username + // Execute the extra actions if any + if td.Form.Extra.SetCookie != nil { + http.SetCookie(w, td.Form.Extra.SetCookie) + } + if td.Form.Extra.RedirectTo != "" { + http.Redirect(w, r, td.Form.Extra.RedirectTo, http.StatusSeeOther) + log.Debug("redirect: %s", td.Form.Extra.RedirectTo) + return // Don't need to render a form if the user is redirected + } + // Remove the recaptcha field if captcha is disabled - if wc.captchaSiteKey == "none" { + if wc.captchaKey() == "none" { for i, field := range td.Form.Fields { if field.Type == forms.FieldTypeCaptcha { td.Form.Fields = append(