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)