Compare commits

..

No commits in common. "01c983244fe18cc9bfdff77cf4bf03c6ebfb48cf" and "3de890936dd350e04c53dda95dcf7584e6f2d997" have entirely different histories.

7 changed files with 98 additions and 165 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
shelves.db shelves.db
shelves.db* shelves.db*
/shelves shelves

View file

@ -1,31 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3"
"git.soup.land/soup/shelves/internal/db"
"git.soup.land/soup/shelves/internal/httpx"
"git.soup.land/soup/shelves/internal/routes"
)
func main() {
db, migrateResult, err := db.Open("./shelves.db")
if err != nil {
log.Fatalf("Failed to open DB: %v", err)
}
if migrateResult.MigrationError != nil {
log.Printf("An error was encountered while upgrading the database schema. You are on version %v, but the latest is %v. The error was: %v", migrateResult.SchemaVerNew, migrateResult.SchemaVerLatest, migrateResult.MigrationError)
}
schemaNeedsUpdating := migrateResult.SchemaVerNew != migrateResult.SchemaVerLatest
if schemaNeedsUpdating {
log.Printf("Your database schema needs to be updated. The application will continue to run, but you may encounter errors.\n")
}
routes := routes.Routes(db)
fmt.Println("Listening on localhost:8999")
http.ListenAndServe("localhost:8999", httpx.Log(routes))
}

View file

@ -1,44 +1,90 @@
package httpx package httpx
import ( import (
"context"
"database/sql"
"git.soup.land/soup/shelves/internal/auth"
"log" "log"
"net/http" "net/http"
"time" "time"
) )
type ResponseWriterTracker struct { type LogWrapper struct {
StatusCode int statusCode *int
Wrote bool
http.ResponseWriter http.ResponseWriter
} }
func (w ResponseWriterTracker) WasWritten() bool { func (w LogWrapper) WriteHeader(statusCode int) {
return w.StatusCode != 0 || w.Wrote *w.statusCode = statusCode
}
func (w *ResponseWriterTracker) WriteHeader(statusCode int) {
w.StatusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode) w.ResponseWriter.WriteHeader(statusCode)
} }
func (w *ResponseWriterTracker) Write(b []byte) (int, error) {
w.Wrote = true
return w.ResponseWriter.Write(b)
}
func NewResponseWriterTracker(w http.ResponseWriter) ResponseWriterTracker {
return ResponseWriterTracker{ResponseWriter: w}
}
func Log(next http.Handler) http.Handler { func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wt := NewResponseWriterTracker(w) statusCode := 200
wrapper := LogWrapper{statusCode: &statusCode, ResponseWriter: w}
start := time.Now() start := time.Now()
next.ServeHTTP(&wt, r) next.ServeHTTP(wrapper, r)
log.Println(r.Method, r.URL.Path, wt.StatusCode, time.Since(start)) log.Println(r.Method, r.URL.Path, statusCode, time.Since(start))
})
}
func queryNeedsOwnerSetup(db *sql.DB) (bool, error) {
count := 0
err := db.QueryRow("select count(*) from owner_settings").Scan(&count)
if err != nil {
return false, err
}
return count != 1, nil
}
func sessionInfo(db *sql.DB, r *http.Request) (auth.SessionInfo, error) {
cookie, err := r.Cookie("SHELVES_OWNER_SESSION_ID")
if err == http.ErrNoCookie {
return auth.SessionInfo{}, nil
}
if err != nil {
return auth.SessionInfo{}, err
}
return auth.SessionCheck(db, cookie.Value)
}
type Ctx struct {
DB *sql.DB
SessionInfo auth.SessionInfo
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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
needsOwnerSetup, err := queryNeedsOwnerSetup(db)
if err != nil {
log.Printf("Error while querying owner_settings: %v\n", err)
}
ctx := r.Context()
session, err := sessionInfo(db, r)
if err != nil {
log.Printf("Error while querying session info: %v\n", err)
}
ctx = context.WithValue(ctx, "__ctx", Ctx{
DB: db,
SessionInfo: session,
NeedsOwnerSetup: needsOwnerSetup,
})
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}) })
} }

View file

@ -3,6 +3,7 @@ package routes
import ( import (
"net/http" "net/http"
"git.soup.land/soup/shelves/internal/httpx"
"git.soup.land/soup/shelves/internal/templates" "git.soup.land/soup/shelves/internal/templates"
"git.soup.land/soup/shelves/internal/templates/components" "git.soup.land/soup/shelves/internal/templates/components"
) )
@ -12,8 +13,8 @@ type homeContent struct {
var homeTmpl = templates.MustParseEmbed("views/home.tmpl.html") var homeTmpl = templates.MustParseEmbed("views/home.tmpl.html")
func homeGet(w http.ResponseWriter, req *http.Request) { func HomeGet(w http.ResponseWriter, req *http.Request) {
ctx := getCtx(req) ctx := httpx.GetCtx(req)
h := components.Page{ h := components.Page{
Title: "Home", Title: "Home",

View file

@ -22,7 +22,7 @@ type loginFormErrors struct {
password error password error
} }
func loginRenderView(f loginForm, e loginFormErrors, ctx Ctx) template.HTML { func loginRenderView(f loginForm, e loginFormErrors, ctx httpx.Ctx) template.HTML {
formHtml := components.Form{ formHtml := components.Form{
Action: "/login", Action: "/login",
Fields: []components.Field{ Fields: []components.Field{
@ -64,8 +64,8 @@ func getRedirectTo(r *http.Request) string {
return redirectTo return redirectTo
} }
func loginGet(w http.ResponseWriter, r *http.Request) { func LoginGet(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
redirectTo := getRedirectTo(r) redirectTo := getRedirectTo(r)
if ctx.SessionInfo.IsAdmin { if ctx.SessionInfo.IsAdmin {
@ -81,8 +81,8 @@ func loginParseForm(f *loginForm, e *loginFormErrors, vs url.Values, v *forms.Va
f.redirectTo = vs.Get("redirectTo") f.redirectTo = vs.Get("redirectTo")
} }
func loginPost(w http.ResponseWriter, r *http.Request) { func LoginPost(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
form := loginForm{} form := loginForm{}
errs := loginFormErrors{} errs := loginFormErrors{}
@ -123,8 +123,8 @@ func loginPost(w http.ResponseWriter, r *http.Request) {
httpx.SeeOther(w, form.redirectTo) httpx.SeeOther(w, form.redirectTo)
} }
func loginDelete(w http.ResponseWriter, r *http.Request) { func LoginDelete(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
httpx.HxRefresh(w) httpx.HxRefresh(w)
if !ctx.SessionInfo.IsAdmin { if !ctx.SessionInfo.IsAdmin {

View file

@ -1,17 +1,12 @@
package routes package routes
import ( import (
"context" "git.soup.land/soup/shelves"
"database/sql" "git.soup.land/soup/shelves/internal/httpx"
"html/template" "html/template"
"log"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"git.soup.land/soup/shelves"
"git.soup.land/soup/shelves/internal/auth"
"git.soup.land/soup/shelves/internal/httpx"
) )
func html(w http.ResponseWriter, s template.HTML) { func html(w http.ResponseWriter, s template.HTML) {
@ -21,7 +16,7 @@ func html(w http.ResponseWriter, s template.HTML) {
func redirectSetup(next http.Handler) http.Handler { func redirectSetup(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
if ctx.NeedsOwnerSetup && !strings.HasPrefix(r.URL.Path, "/static") { if ctx.NeedsOwnerSetup && !strings.HasPrefix(r.URL.Path, "/static") {
if r.URL.Path != "/setup" { if r.URL.Path != "/setup" {
httpx.SeeOther(w, "/setup") httpx.SeeOther(w, "/setup")
@ -33,7 +28,7 @@ func redirectSetup(next http.Handler) http.Handler {
}) })
} }
func serveStatic() http.Handler { func ServeStatic() http.Handler {
inner := http.StripPrefix("/static/", http.FileServerFS(shelves.Frontend)) inner := http.StripPrefix("/static/", http.FileServerFS(shelves.Frontend))
startup := time.Now().UTC() startup := time.Now().UTC()
@ -52,93 +47,15 @@ func serveStatic() http.Handler {
}) })
} }
func seqHandler(a, b http.Handler) http.Handler { func Routes() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux := http.NewServeMux()
wt := httpx.NewResponseWriterTracker(w) mux.Handle("GET /static/", ServeStatic())
mux.HandleFunc("GET /{$}", HomeGet)
mux.HandleFunc("GET /settings", SettingsGet)
mux.HandleFunc("POST /settings", SettingsPost)
mux.HandleFunc("GET /login", LoginGet)
mux.HandleFunc("POST /login", LoginPost)
mux.HandleFunc("DELETE /login", LoginDelete)
a.ServeHTTP(&wt, r) return redirectSetup(mux)
if !wt.WasWritten() {
b.ServeHTTP(&wt, r)
}
})
}
func Routes(db *sql.DB) http.Handler {
public := http.NewServeMux()
public.Handle("GET /static/", serveStatic())
public.HandleFunc("GET /{$}", homeGet)
public.HandleFunc("GET /login", loginGet)
public.HandleFunc("POST /login", loginPost)
public.HandleFunc("DELETE /login", loginDelete)
public.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
ownerOnly := http.NewServeMux()
ownerOnly.HandleFunc("GET /settings", settingsGet)
ownerOnly.HandleFunc("POST /settings", settingsPost)
ownerOnly.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
h := seqHandler(public, ownerOnly)
h = seqHandler(h, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 not found"))
}))
return withCtx(db, redirectSetup(h))
}
func queryNeedsOwnerSetup(db *sql.DB) (bool, error) {
count := 0
err := db.QueryRow("select count(*) from owner_settings").Scan(&count)
if err != nil {
return false, err
}
return count != 1, nil
}
func sessionInfo(db *sql.DB, r *http.Request) (auth.SessionInfo, error) {
cookie, err := r.Cookie("SHELVES_OWNER_SESSION_ID")
if err == http.ErrNoCookie {
return auth.SessionInfo{}, nil
}
if err != nil {
return auth.SessionInfo{}, err
}
return auth.SessionCheck(db, cookie.Value)
}
type Ctx struct {
DB *sql.DB
SessionInfo auth.SessionInfo
NeedsOwnerSetup bool
}
func getCtx(r *http.Request) Ctx {
cx := r.Context()
return cx.Value("__ctx").(Ctx)
}
func withCtx(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
needsOwnerSetup, err := queryNeedsOwnerSetup(db)
if err != nil {
log.Printf("Error while querying owner_settings: %v\n", err)
}
ctx := r.Context()
session, err := sessionInfo(db, r)
if err != nil {
log.Printf("Error while querying session info: %v\n", err)
}
ctx = context.WithValue(ctx, "__ctx", Ctx{
DB: db,
SessionInfo: session,
NeedsOwnerSetup: needsOwnerSetup,
})
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
} }

View file

@ -14,7 +14,7 @@ import (
"git.soup.land/soup/shelves/internal/templates/components" "git.soup.land/soup/shelves/internal/templates/components"
) )
func settingsRenderView(f settingsForm, e settingsFormErrors, ctx Ctx) template.HTML { func settingsRenderView(f settingsForm, e settingsFormErrors, ctx httpx.Ctx) template.HTML {
body := components.Form{ body := components.Form{
Action: "/settings", Action: "/settings",
Fields: []components.Field{ Fields: []components.Field{
@ -54,8 +54,8 @@ func queryGetOwnerSettings(db *sql.DB) (settingsForm, error) {
return form, err return form, err
} }
func settingsGet(w http.ResponseWriter, r *http.Request) { func SettingsGet(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
if !ctx.SessionInfo.IsAdmin && !ctx.NeedsOwnerSetup { if !ctx.SessionInfo.IsAdmin && !ctx.NeedsOwnerSetup {
httpx.LoginRedirect(w, *r.URL) httpx.LoginRedirect(w, *r.URL)
return return
@ -113,8 +113,8 @@ func updateOwnerSettings(db *sql.DB, f settingsForm) error {
return queryUpdateOwnerSettings(db, f.displayName, salt, hash) return queryUpdateOwnerSettings(db, f.displayName, salt, hash)
} }
func settingsPost(w http.ResponseWriter, r *http.Request) { func SettingsPost(w http.ResponseWriter, r *http.Request) {
ctx := getCtx(r) ctx := httpx.GetCtx(r)
if !ctx.SessionInfo.IsAdmin && !ctx.NeedsOwnerSetup { if !ctx.SessionInfo.IsAdmin && !ctx.NeedsOwnerSetup {
httpx.Unauthorized(w) httpx.Unauthorized(w)