summaryrefslogtreecommitdiff
path: root/pages
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.eu>2024-10-17 00:27:08 +0200
committerRené 'Necoro' Neumann <necoro@necoro.eu>2024-10-17 00:27:08 +0200
commit869fb9691f877116d5b15a92de006d0daf4d70e5 (patch)
tree2493c72172d5817ec9deec36229a84b687eb3190 /pages
parent6fc180ba6d9bc5c32340466988d9e26f8d6e3c5c (diff)
downloadgosten-869fb9691f877116d5b15a92de006d0daf4d70e5.tar.gz
gosten-869fb9691f877116d5b15a92de006d0daf4d70e5.tar.bz2
gosten-869fb9691f877116d5b15a92de006d0daf4d70e5.zip
Restructure and change to chi as muxing framework
Diffstat (limited to 'pages')
-rw-r--r--pages/login.go123
-rw-r--r--pages/logout.go15
-rw-r--r--pages/page.go58
-rw-r--r--pages/pages.go26
4 files changed, 222 insertions, 0 deletions
diff --git a/pages/login.go b/pages/login.go
new file mode 100644
index 0000000..fb7859a
--- /dev/null
+++ b/pages/login.go
@@ -0,0 +1,123 @@
+package pages
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "gosten/csrf"
+ "gosten/form"
+ "gosten/session"
+ "log"
+ "net/http"
+ "net/url"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+type userContextKey struct{}
+
+const (
+ sessionDuration = 86400 * 7 // 7 days
+ loginQueryMarker = "next"
+)
+
+func RequireAuth(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ s := session.From(r)
+
+ if !s.IsNew() && s.Authenticated {
+ u, err := Q.GetUserById(r.Context(), s.UserID)
+ if err == nil {
+ // authenticated --> done
+ ctx := context.WithValue(r.Context(), userContextKey{}, u.ID)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ return
+ }
+
+ s.Invalidate()
+ s.Save(w, r)
+ }
+
+ // redirect to login with next-param
+ v := url.Values{}
+ v.Set(loginQueryMarker, r.URL.Path)
+ redirPath := "/login?" + v.Encode()
+ http.Redirect(w, r, redirPath, http.StatusFound)
+ })
+}
+
+type User struct {
+ Name string `form:"options=required,autofocus"`
+ Password string `form:"type=password;options=required"`
+ RememberMe bool `form:"type=checkbox;value=y;options=checked"`
+ Errors []error `form:"-"`
+ csrf.Csrf
+}
+
+func showLoginPage(w http.ResponseWriter, u User) {
+ showTemplate(w, "login", u)
+}
+
+func Login() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if session.From(r).Authenticated {
+ http.Redirect(w, r, "/", http.StatusFound)
+ }
+ u := User{}
+ u.SetCsrfField(r)
+ showLoginPage(w, u)
+ }
+}
+
+func userId(r *http.Request) int32 {
+ return r.Context().Value(userContextKey{}).(int32)
+}
+
+func checkLogin(ctx context.Context, user User) (bool, int32) {
+ dbUser, err := Q.GetUserByName(ctx, user.Name)
+ if err == nil {
+ hash := []byte(dbUser.Pwd)
+ pwd := []byte(user.Password)
+
+ if bcrypt.CompareHashAndPassword(hash, pwd) != nil {
+ return false, 0
+ }
+ } else if errors.Is(err, sql.ErrNoRows) {
+ return false, 0
+ } else {
+ log.Panicf("Could not load user '%s': %v", user.Name, err)
+ }
+
+ return true, dbUser.ID
+}
+
+func HandleLogin(w http.ResponseWriter, r *http.Request) {
+ u := User{}
+ form.Parse(r, &u)
+
+ ok, userId := checkLogin(r.Context(), u)
+
+ if !ok {
+ u.Errors = []error{form.FieldError{Field: "Password", Issue: "Invalid"}}
+ showLoginPage(w, u)
+ return
+ }
+
+ s := session.From(r)
+ if u.RememberMe {
+ s.MaxAge(sessionDuration) // 1 week
+ } else {
+ s.MaxAge(0)
+ }
+
+ s.UserID = userId
+ s.Authenticated = true
+ s.Save(w, r)
+
+ // redirect
+ next := r.URL.Query().Get(loginQueryMarker)
+ if next == "" {
+ next = "/"
+ }
+ http.Redirect(w, r, next, http.StatusFound)
+}
diff --git a/pages/logout.go b/pages/logout.go
new file mode 100644
index 0000000..dad0e1a
--- /dev/null
+++ b/pages/logout.go
@@ -0,0 +1,15 @@
+package pages
+
+import (
+ "gosten/session"
+ "net/http"
+)
+
+func Logout() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s := session.From(r)
+ s.Invalidate()
+ s.Save(w, r)
+ http.Redirect(w, r, "/", http.StatusFound)
+ }
+}
diff --git a/pages/page.go b/pages/page.go
new file mode 100644
index 0000000..25c2331
--- /dev/null
+++ b/pages/page.go
@@ -0,0 +1,58 @@
+package pages
+
+import (
+ "context"
+ "gosten/model"
+ "gosten/templ"
+ "log"
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+)
+
+var Q *model.Queries
+
+func Connect(tx model.DBTX) {
+ Q = model.New(tx)
+}
+
+type Page interface {
+ http.Handler
+}
+
+type dataFunc func(r *http.Request, uid int32) any
+
+type simplePage struct {
+ dataFn dataFunc
+ template string
+}
+
+func (p simplePage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ input := p.dataFn(r, userId(r))
+ p.showTemplate(w, input)
+}
+
+func simpleByQuery[T any](tpl string, query func(ctx context.Context, id int32) (T, error)) Page {
+ dataFn := func(r *http.Request, uid int32) any {
+ d, _ := query(r.Context(), uid)
+ return d
+ }
+ return simple(tpl, dataFn)
+}
+
+func simple(tpl string, dataFn dataFunc) Page {
+ p := simplePage{dataFn, tpl}
+ r := chi.NewRouter()
+ r.Get("/", p.ServeHTTP)
+ return r
+}
+
+func showTemplate(w http.ResponseWriter, tpl string, data any) {
+ if err := templ.Lookup(tpl).Execute(w, data); err != nil {
+ log.Panicf("Executing '%s' with %+v: %v", tpl, data, err)
+ }
+}
+
+func (p simplePage) showTemplate(w http.ResponseWriter, data any) {
+ showTemplate(w, p.template, data)
+}
diff --git a/pages/pages.go b/pages/pages.go
new file mode 100644
index 0000000..e965bdd
--- /dev/null
+++ b/pages/pages.go
@@ -0,0 +1,26 @@
+package pages
+
+import (
+ "net/http"
+)
+
+func Init() Page {
+ return simple("index", func(r *http.Request, uid int32) any {
+ u, _ := Q.GetUserById(r.Context(), uid)
+ return u.Name
+ })
+}
+
+func Recur() Page {
+ return simpleByQuery("recur", Q.GetRecurExpenses)
+}
+
+func Categories() Page {
+ return simpleByQuery("categories", Q.GetCategoriesOrdered)
+}
+
+func NotFound() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ showTemplate(w, "404", r.RequestURI)
+ }
+}