.
This commit is contained in:
parent
1ca54d6114
commit
329f28a737
2
Makefile
2
Makefile
|
|
@ -7,4 +7,4 @@ run:
|
|||
go run backend/bin/shelves.go
|
||||
|
||||
watch:
|
||||
fd . . | entr -cr make run
|
||||
fd | entr -cr make run
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
10
backend/errorsx/errorsx.go
Normal file
10
backend/errorsx/errorsx.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package errorsx
|
||||
|
||||
func String(err error) string {
|
||||
out := ""
|
||||
if err != nil {
|
||||
out = err.Error()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
40
backend/forms/forms.go
Normal file
40
backend/forms/forms.go
Normal file
|
|
@ -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
|
||||
}
|
||||
10
backend/httpx/httpx.go
Normal file
10
backend/httpx/httpx.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
|
||||
<form action="{{.Action}}" method="{{.Method}}">
|
||||
{{range .Fields}}
|
||||
<label>
|
||||
{{.Label}}
|
||||
{{if eq .Type "textarea"}}
|
||||
<textarea
|
||||
{{template "attrs" .}}
|
||||
>{{.Value}}</textarea>
|
||||
{{else}}
|
||||
<input
|
||||
type="{{.Type}}"
|
||||
value="{{.Value}}"
|
||||
{{template "attrs" .}}
|
||||
>
|
||||
<mu-field>
|
||||
<label>
|
||||
{{.Label}}
|
||||
{{if eq .Type "textarea"}}
|
||||
<textarea
|
||||
{{template "attrs" .}}
|
||||
>{{.Value}}</textarea>
|
||||
{{else}}
|
||||
<input
|
||||
type="{{.Type}}"
|
||||
value="{{.Value}}"
|
||||
{{template "attrs" .}}
|
||||
>
|
||||
{{end}}
|
||||
</label>
|
||||
{{if .Error}}
|
||||
<small id="{{.Name}}-error">{{.Error}}</small>
|
||||
{{end}}
|
||||
</label>
|
||||
{{if .Error}}
|
||||
<small id="{{.Name}}-error">{{.Error}}</small>
|
||||
{{end}}
|
||||
</mu-field>
|
||||
{{end}}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{{.Title}} - Shelves</title>
|
||||
<link rel="stylesheet" href="/static/css/mu.css" />
|
||||
{{.Head}}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
20
flake.lock
20
flake.lock
|
|
@ -24,7 +24,7 @@
|
|||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
"nixpkgs-unstable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
|
|
@ -54,11 +54,27 @@
|
|||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1731685661,
|
||||
"narHash": "sha256-5ktjIlkTfWNbbrit9refiRY5rKR4nw9oTe5ck/YUNsQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2d088b482efb193d02d1692697f9dd0b6f8b5012",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
|
|
|||
12
flake.nix
12
flake.nix
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
|
||||
inputs.nixpkgs.url = "nixpkgs";
|
||||
inputs.nixpkgs-unstable.url = "github:NixOS/nixpkgs?branch=nixpkgs-unstable";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.gomod2nix = {
|
||||
url = "github:nix-community/gomod2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||
inputs.flake-utils.follows = "flake-utils";
|
||||
};
|
||||
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
(inputs.flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
pkgsU = inputs.nixpkgs-unstable.legacyPackages.${system};
|
||||
pkgsG = inputs.gomod2nix.legacyPackages.${system};
|
||||
in {
|
||||
devShells.default =
|
||||
|
|
@ -21,9 +22,12 @@
|
|||
gnumake
|
||||
entr
|
||||
fd
|
||||
goEnv
|
||||
(sqlite.override { interactive = true; })
|
||||
pkgsG.gomod2nix
|
||||
pkgsU.go_1_23
|
||||
pkgsU.gopls
|
||||
# TODO: re-enable this once it works with go 1.23
|
||||
# goEnv
|
||||
# pkgsG.gomod2nix
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package shelves
|
||||
|
||||
import "embed"
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed frontend/*
|
||||
var Frontend embed.FS
|
||||
var fe embed.FS
|
||||
|
||||
var Frontend, _ = fs.Sub(fe, "frontend")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
form {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 1rem;
|
||||
|
||||
& input {
|
||||
width: 100%;
|
||||
|
||||
&[aria-invalid="true"] {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
& label:has([aria-invalid="true"]) + small {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue