diff --git a/.fdignore b/.fdignore new file mode 120000 index 0000000..3e4e48b --- /dev/null +++ b/.fdignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/Makefile b/Makefile index 0b5a80b..38fdeac 100644 --- a/Makefile +++ b/Makefile @@ -7,4 +7,4 @@ run: go run backend/bin/shelves.go watch: - fd . . | entr -cr make run + fd | entr -cr make run diff --git a/backend/bin/shelves.go b/backend/bin/shelves.go index be32810..38f2ad3 100644 --- a/backend/bin/shelves.go +++ b/backend/bin/shelves.go @@ -9,7 +9,7 @@ import ( _ "github.com/mattn/go-sqlite3" "shelves/backend/db" - shelvesHttp "shelves/backend/http" + "shelves/backend/httpx" "shelves/backend/routes" ) @@ -30,5 +30,5 @@ func main() { routes := routes.Routes() fmt.Println("Listening on localhost:8999") - http.ListenAndServe("localhost:8999", shelvesHttp.Log(shelvesHttp.WithCtx(db, routes))) + http.ListenAndServe("localhost:8999", httpx.Log(httpx.WithCtx(db, routes))) } diff --git a/backend/errorsx/errorsx.go b/backend/errorsx/errorsx.go new file mode 100644 index 0000000..2441657 --- /dev/null +++ b/backend/errorsx/errorsx.go @@ -0,0 +1,10 @@ +package errorsx + +func String(err error) string { + out := "" + if err != nil { + out = err.Error() + } + + return out +} diff --git a/backend/forms/forms.go b/backend/forms/forms.go new file mode 100644 index 0000000..c079ae4 --- /dev/null +++ b/backend/forms/forms.go @@ -0,0 +1,40 @@ +package forms + +import ( + "errors" + "fmt" + "net/http" + "net/url" +) + +type Validator struct { + Failed bool +} + +func (v *Validator) MinLength(s string, min int) error { + var err error = nil + if len(s) < min { + v.Failed = true + err = fmt.Errorf("Minimum length: %v", min) + } + + return err +} + +func (v *Validator) Fail(s string) error { + v.Failed = true + + return errors.New(s) +} + +func ParseFormData(r *http.Request, parse func(url.Values, *Validator)) bool { + v := Validator{} + err := r.ParseForm() + if err != nil { + return true + } + + parse(r.Form, &v) + + return v.Failed +} diff --git a/backend/httpx/httpx.go b/backend/httpx/httpx.go new file mode 100644 index 0000000..c2acabf --- /dev/null +++ b/backend/httpx/httpx.go @@ -0,0 +1,10 @@ +package httpx + +import ( + "net/http" +) + +func SeeOther(w http.ResponseWriter, location string) { + w.Header().Add("Location", location) + w.WriteHeader(http.StatusSeeOther) +} diff --git a/backend/http/middleware.go b/backend/httpx/middleware.go similarity index 86% rename from backend/http/middleware.go rename to backend/httpx/middleware.go index 0379549..cf828f6 100644 --- a/backend/http/middleware.go +++ b/backend/httpx/middleware.go @@ -1,4 +1,4 @@ -package http +package httpx import ( "context" @@ -6,8 +6,6 @@ import ( "log" "net/http" "time" - - "shelves/backend/routes" ) type LogWrapper struct { @@ -68,8 +66,15 @@ func queryNeedsOwnerSetup(db *sql.DB) (bool, error) { } type Ctx struct { - DB *sql.DB - Auth AuthInfo + DB *sql.DB + Auth AuthInfo + NeedsOwnerSetup bool +} + +func GetCtx(r *http.Request) Ctx { + ctx := r.Context() + + return ctx.Value("__ctx").(Ctx) } func WithCtx(db *sql.DB, next http.Handler) http.Handler { @@ -79,25 +84,15 @@ func WithCtx(db *sql.DB, next http.Handler) http.Handler { log.Printf("Error while querying owner_settings: %v\n", err) } - if needsOwnerSetup { - if r.URL.Path != "/setup" { - w.Header().Add("Location", "/setup") - w.WriteHeader(http.StatusSeeOther) - return - } else { - routes.SetupGet(w, r) - return - } - } - ctx := r.Context() auth, err := checkAuthed(db, r) if err != nil { log.Printf("Error while querying auth info: %v\n", err) } ctx = context.WithValue(ctx, "__ctx", Ctx{ - DB: db, - Auth: auth, + DB: db, + Auth: auth, + NeedsOwnerSetup: needsOwnerSetup, }) r = r.WithContext(ctx) diff --git a/backend/routes/routes.go b/backend/routes/routes.go index ce6a782..c9abf06 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -1,9 +1,10 @@ package routes import ( - "embed" "html/template" "net/http" + "shelves/backend/httpx" + "strings" "shelves" ) @@ -13,10 +14,26 @@ func html(w http.ResponseWriter, s template.HTML) { w.Write([]byte(s)) } -func Routes() *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc("GET /", HomeGet) - mux.Handle("GET /static", http.FileServerFS(shelves.Frontend)) +func redirectSetup(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := httpx.GetCtx(r) + if ctx.NeedsOwnerSetup && !strings.HasPrefix(r.URL.Path, "/static") { + if r.URL.Path != "/setup" { + httpx.SeeOther(w, "/setup") + return + } + } - return mux + next.ServeHTTP(w, r) + }) +} + +func Routes() http.Handler { + mux := http.NewServeMux() + mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(shelves.Frontend))) + mux.HandleFunc("GET /", HomeGet) + mux.HandleFunc("GET /setup", SetupGet) + mux.HandleFunc("POST /setup", SetupPost) + + return redirectSetup(mux) } diff --git a/backend/routes/setup.go b/backend/routes/setup.go index c7a3db7..6a564dc 100644 --- a/backend/routes/setup.go +++ b/backend/routes/setup.go @@ -1,24 +1,84 @@ package routes import ( + "html/template" "net/http" + "net/url" + "shelves/backend/errorsx" + "shelves/backend/forms" + "shelves/backend/httpx" "shelves/backend/templates" ) -func SetupGet(w http.ResponseWriter, r *http.Request) { - html(w, templates.PageBase{Title: "Set up", Body: templates.Form{ +func renderView(f setupForm, e setupFormErrors) template.HTML { + + body := templates.Form{ Action: "/setup", Fields: []templates.Field{ templates.Field{ Label: "Password", Type: "password", Name: "password", + Error: errorsx.String(e.password), }, templates.Field{ Label: "Confirm password", Type: "password", - Name: "password-confirmation", + Name: "passwordConfirmation", }, }, - }.HTML()}.HTML()) + } + + return templates.PageBase{Title: "Set up", Body: body.HTML()}.HTML() +} + +func SetupGet(w http.ResponseWriter, r *http.Request) { + ctx := httpx.GetCtx(r) + if !ctx.NeedsOwnerSetup { + httpx.SeeOther(w, "/") + return + } + + html(w, renderView(setupForm{}, setupFormErrors{})) +} + +type setupForm struct { + password string +} + +type setupFormErrors struct { + password error +} + +func parseForm(f *setupForm, e *setupFormErrors, vs url.Values, v *forms.Validator) { + f.password = vs.Get("password") + passwordConfirmation := vs.Get("passwordConfirmation") + + if f.password != passwordConfirmation { + e.password = v.Fail("Passwords did not match") + return + } + + e.password = v.MinLength(f.password, 1) +} + +func SetupPost(w http.ResponseWriter, r *http.Request) { + ctx := httpx.GetCtx(r) + if !ctx.NeedsOwnerSetup { + httpx.SeeOther(w, "/") + return + } + + form := setupForm{} + errs := setupFormErrors{} + failed := forms.ParseFormData(r, func(vs url.Values, v *forms.Validator) { + parseForm(&form, &errs, vs, v) + }) + + if failed { + w.WriteHeader(http.StatusBadRequest) + html(w, renderView(form, errs)) + } else { + httpx.SeeOther(w, "/setup") + } } diff --git a/backend/templates/form.go b/backend/templates/form.go index 781e114..ab78c9a 100644 --- a/backend/templates/form.go +++ b/backend/templates/form.go @@ -1,6 +1,8 @@ package templates -import "html/template" +import ( + "html/template" +) type Field struct { Label string @@ -31,8 +33,8 @@ func (f Form) HTML() template.HTML { f.Method = "POST" } - for _, f := range f.Fields { - f.init() + for i := range f.Fields { + f.Fields[i].init() } return tmpls.renderHtml("form.tmpl.html", f) diff --git a/backend/templates/tmpls/form.tmpl.html b/backend/templates/tmpls/form.tmpl.html index ca4d7f0..6042e2d 100644 --- a/backend/templates/tmpls/form.tmpl.html +++ b/backend/templates/tmpls/form.tmpl.html @@ -1,7 +1,8 @@ {{define "attrs"}} + name="{{.Name}}" placeholder="{{.Placeholder}}" - aria-invalid="{{.Valid}}" + aria-invalid="{{not .Valid}}" {{if .Error}} aria-describedby="{{.Name}}-error" {{end}} @@ -9,22 +10,25 @@
diff --git a/backend/templates/tmpls/page_base.tmpl.html b/backend/templates/tmpls/page_base.tmpl.html index 72a6b3b..69e8840 100644 --- a/backend/templates/tmpls/page_base.tmpl.html +++ b/backend/templates/tmpls/page_base.tmpl.html @@ -2,6 +2,7 @@