Merge branch 'easy-forms'

This commit is contained in:
2019-03-31 22:35:01 +02:00
13 changed files with 646 additions and 119 deletions

127
webcontroller/forms/form.go Normal file
View File

@@ -0,0 +1,127 @@
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
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
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
// Actions to perform when the form is rendered
Extra ExtraActions
}
// 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. 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 == 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
// Fields which can be in a form
const (
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
for i, field := range f.Fields {
field.EnteredValue = r.FormValue(field.Name)
if field.DefaultValue == "" {
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
}
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))
}

View File

@@ -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 {

View File

@@ -1,9 +1,12 @@
package webcontroller
import (
"html/template"
"net/http"
"time"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
@@ -37,3 +40,213 @@ func (wc *WebController) serveLogout(
http.Redirect(w, r, "/", http.StatusSeeOther)
}
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{
{
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.captchaKey(),
},
},
BackLink: "/",
SubmitLabel: "Register",
PostFormHTML: template.HTML("<p>Welcome to the club!</p>"),
}
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
}
log.Debug("capt: %s", f.FieldVal("recaptcha_response"))
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 <a href="/login">log in ` +
`to your account</a>.<br/>We're glad to have you on ` +
`board, have fun sharing!`}
}
}
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(
`<br/>If you don't have a pixeldrain account yet, you can ` +
`<a href="/register">register here</a>. No e-mail address is ` +
`required.<br/>`,
),
}
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{
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
}

View File

@@ -8,6 +8,7 @@ import (
"fornaxian.com/pixeldrain-web/init/conf"
"fornaxian.com/pixeldrain-web/pixelapi"
"fornaxian.com/pixeldrain-web/webcontroller/forms"
"github.com/Fornaxian/log"
"github.com/julienschmidt/httprouter"
)
@@ -48,20 +49,24 @@ 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+"/login" /* */, wc.serveTemplate("login", false))
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.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))
@@ -69,6 +74,11 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon
r.GET(p+"/user/lists" /* */, wc.serveTemplate("user_lists", true))
r.GET(p+"/user/filemanager" /**/, wc.serveTemplate("file_manager", true))
// 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)
return wc
@@ -105,6 +115,66 @@ 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
// 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.captchaKey() == "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)