Compare commits
No commits in common. "01c983244fe18cc9bfdff77cf4bf03c6ebfb48cf" and "3de890936dd350e04c53dda95dcf7584e6f2d997" have entirely different histories.
01c983244f
...
3de890936d
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
shelves.db
|
shelves.db
|
||||||
shelves.db*
|
shelves.db*
|
||||||
/shelves
|
shelves
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue