~damien/shopping

8d5c97277c87ed7ca87f3440335a8f7620ea20f5 — Damien Radtke 2 years ago 12d948a
Simplify route handling
9 files changed, 149 insertions(+), 174 deletions(-)

M README.md
M cmd/web/main.go
M web/index.go
M web/login.go
M web/logout.go
M web/routes.go
A web/server.go
R web/{web.go => session.go}
D web/template.go
M README.md => README.md +2 -0
@@ 6,6 6,8 @@ This is an experimental design for a cross-platform app/website/service/thing in
$ overmind start
```

TOOD: Set up a command to pull docker images in advance so that this doesn't fail due to timeouts.

Web:

```bash

M cmd/web/main.go => cmd/web/main.go +1 -1
@@ 21,7 21,7 @@ func run() error {
	addr := httputil.Addr()
	log.Printf("== serving web on %s (debug mode: %t) ==", addr, DEBUG)

	server := web.Server(addr, DEBUG)
	server := web.NewServer(addr, DEBUG)
	done := make(chan struct{})
	httputil.HandleSignals(server, 2*time.Second, done)
	defer func() {

M web/index.go => web/index.go +16 -19
@@ 4,30 4,27 @@ import (
	"html/template"
	"net/http"

	"github.com/gorilla/sessions"
	"golang.org/x/text/language"

	"shopping.io"
	"shopping.io/httputil"
)

type IndexHandler struct {
	Templated
	sessionStore sessions.Store
}

func (h IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s := GetSession(h.sessionStore, r)
func (s Server) Index() http.HandlerFunc {
	var t *template.Template
	return s.withTemplates(&t, []string{"index/index.html"}, func(w http.ResponseWriter, r *http.Request) {
		s := GetSession(s.sessionStore, r)

	if err := h.t.ExecuteTemplate(w, "index.html", struct {
		Lang   language.Tag
		CSRF   template.HTML
		UserID shopping.ID
	}{
		Lang:   httputil.LanguageKey(r.Context()),
		CSRF:   httputil.CsrfField(r.Context()),
		UserID: s.UserID(),
	}); err != nil {
		panic(err)
	}
		if err := t.ExecuteTemplate(w, "index.html", struct {
			Lang   language.Tag
			CSRF   template.HTML
			UserID shopping.ID
		}{
				Lang:   httputil.LanguageKey(r.Context()),
				CSRF:   httputil.CsrfField(r.Context()),
				UserID: s.UserID(),
			}); err != nil {
			panic(err)
		}
	})
}

M web/login.go => web/login.go +46 -51
@@ 4,70 4,65 @@ import (
	"html/template"
	"net/http"

	"github.com/gorilla/sessions"
	"golang.org/x/text/language"

	"shopping.io"
	"shopping.io/api/apiclient"
	"shopping.io/busi/auth"
	"shopping.io/httputil"
)

type LoginHandler struct {
	Templated
	sessionStore sessions.Store
	ac           apiclient.Client
}
func (s Server) Login() http.HandlerFunc {
	var t *template.Template
	return s.withTemplates(&t, []string{"login/login.html"}, func(w http.ResponseWriter, r *http.Request) {
		session := GetSession(s.sessionStore, r)

func (h LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s := GetSession(h.sessionStore, r)
		switch r.Method {
		case http.MethodGet:
			if !session.UserID().Zero() {
				http.Redirect(w, r, "/", http.StatusFound)
				return
			}

	switch r.Method {
	case http.MethodGet:
		if !s.UserID().Zero() {
			http.Redirect(w, r, "/", http.StatusFound)
			return
		}
			if err := t.ExecuteTemplate(w, "login.html", struct {
				Lang language.Tag
				CSRF template.HTML
			}{
					Lang: httputil.LanguageKey(r.Context()),
					CSRF: httputil.CsrfField(r.Context()),
				}); err != nil {
				panic(err)
			}

		if err := h.t.ExecuteTemplate(w, "login.html", struct {
			Lang language.Tag
			CSRF template.HTML
		}{
			Lang: httputil.LanguageKey(r.Context()),
			CSRF: httputil.CsrfField(r.Context()),
		}); err != nil {
			panic(err)
		}
		case http.MethodPost:
			req := auth.LoginReq{
				Username: r.PostFormValue("Username"),
				Password: r.PostFormValue("Password"),
			}
			if req.Username == "" || req.Password == "" {
				httputil.Log(r.Context(), "login form incomplete")
				http.Redirect(w, r, r.URL.Path, http.StatusFound)
				return
			}

	case http.MethodPost:
		req := auth.LoginReq{
			Username: r.PostFormValue("Username"),
			Password: r.PostFormValue("Password"),
		}
		if req.Username == "" || req.Password == "" {
			httputil.Log(r.Context(), "login form incomplete")
			http.Redirect(w, r, r.URL.Path, http.StatusFound)
			return
		}
			httputil.Log(r.Context(), "attempting login as "+req.Username)

		httputil.Log(r.Context(), "attempting login as "+req.Username)
			var userID shopping.ID
			if err := s.api.Call(r.Context(), "Auth.Login", req, &userID); err != nil {
				panic(err)
			}

		var userID shopping.ID
		if err := h.ac.Call(r.Context(), "Auth.Login", req, &userID); err != nil {
			panic(err)
		}
			if !userID.Zero() {
				httputil.Log(r.Context(), "login succeeded")
				session.SetUserID(userID)
				session.Save(w, r)
				http.Redirect(w, r, "/", http.StatusFound)
			} else {
				httputil.Log(r.Context(), "login failed")
				http.Redirect(w, r, r.URL.Path, http.StatusFound)
			}

		if !userID.Zero() {
			httputil.Log(r.Context(), "login succeeded")
			s.SetUserID(userID)
			s.Save(w, r)
			http.Redirect(w, r, "/", http.StatusFound)
		} else {
			httputil.Log(r.Context(), "login failed")
			http.Redirect(w, r, r.URL.Path, http.StatusFound)
		default:
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		}

	default:
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}
	})
}

M web/logout.go => web/logout.go +10 -14
@@ 2,22 2,18 @@ package web

import (
	"net/http"

	"github.com/gorilla/sessions"
)

type LogoutHandler struct {
	sessionStore sessions.Store
}
func (s Server) Logout() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
			return
		}

func (h LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		return
		session := GetSession(s.sessionStore, r)
		session.ClearUserID()
		session.Save(w, r)
		http.Redirect(w, r, "/", http.StatusFound)
	}

	s := GetSession(h.sessionStore, r)
	s.ClearUserID()
	s.Save(w, r)
	http.Redirect(w, r, "/", http.StatusFound)
}

M web/routes.go => web/routes.go +4 -21
@@ 1,24 1,7 @@
package web

import (
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"

	"shopping.io/api/apiclient"
)

func registerRoutes(router *mux.Router, store sessions.Store) {
	const templateFolder = "web/front"
	ac := apiclient.New("web")

	router.Handle("/", &IndexHandler{
		Templated:    Templates(templateFolder + "/index/index.html"),
		sessionStore: store,
	})
	router.Handle("/login", &LoginHandler{
		Templated:    Templates(templateFolder + "/login/login.html"),
		sessionStore: store,
		ac:           ac,
	})
	router.Handle("/logout", &LogoutHandler{sessionStore: store})
func (s Server) registerRoutes() {
	s.router.Handle("/", s.Index())
	s.router.Handle("/login", s.Login())
	s.router.Handle("/logout", s.Logout())
}

A web/server.go => web/server.go +69 -0
@@ 0,0 1,69 @@
// Package web implements the backend for the web-based client.
package web

import (
	"net/http"
	"html/template"

	"golang.org/x/text/language"
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"

	"shopping.io/i18n"
	"shopping.io/httputil"
	"shopping.io/api/apiclient"
)

type Server struct {
	debug bool
	router *mux.Router
	sessionStore sessions.Store
	api apiclient.Client
}

func NewServer(addr string, debug bool) *http.Server {
	s := Server{
		debug: debug,
		router: mux.NewRouter(),
		sessionStore: newSessionStore(debug),
	}
	s.router.Use(httputil.SetRequestID)
	s.router.Use(httputil.Recover)
	s.router.Use(httputil.CSRFMiddleware(debug))
	s.router.Use(httputil.MatchLanguage)
	if debug {
		s.router.Use(httputil.ReloadTemplates)
	}
	s.registerRoutes()
	return &http.Server{
		Addr:    addr,
		Handler: s.router,
	}
}

func (s Server) withTemplates(t **template.Template, paths []string, next http.HandlerFunc) http.HandlerFunc {
	const templateFolder = "web/front"
	for i := range paths {
		paths[i] = templateFolder + "/" + paths[i]
	}
	loadTemplate := func() {
		*t = template.Must(template.New("").Funcs(s.templateFuncs()).ParseFiles(paths...))
	}
	loadTemplate()
	return func(w http.ResponseWriter, r *http.Request) {
		if s.debug {
			loadTemplate()
		}
		next(w, r)
	}
}

func (s Server) templateFuncs() template.FuncMap {
	return template.FuncMap{
		"text": s.tmplText,
	}
}

func (s Server) tmplText(lang language.Tag, text string, args ...interface{}) string {
	return i18n.Text(lang, text, args...)
}

R web/web.go => web/session.go +1 -24
@@ 1,38 1,15 @@
// Package web implements the backend for the web-based client.
package web

import (
	"net/http"
	"os"

	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"

	"shopping.io"
	"shopping.io/httputil"
)

func Server(addr string, debug bool) *http.Server {
	return &http.Server{
		Addr:    addr,
		Handler: Handler(debug, sessionStore(debug)),
	}
}

func Handler(debug bool, store sessions.Store) http.Handler {
	router := mux.NewRouter()
	router.Use(httputil.SetRequestID)
	router.Use(httputil.Recover)
	router.Use(httputil.CSRFMiddleware(debug))
	router.Use(httputil.MatchLanguage)
	if debug {
		router.Use(httputil.ReloadTemplates)
	}
	registerRoutes(router, store)
	return router
}

func sessionStore(debug bool) sessions.Store {
func newSessionStore(debug bool) sessions.Store {
	var key []byte
	if debug {
		key = make([]byte, 32)

D web/template.go => web/template.go +0 -44
@@ 1,44 0,0 @@
package web

import (
	"html/template"

	"golang.org/x/text/language"
	"shopping.io/httputil"
	"shopping.io/i18n"
)

// dependencies that the template functions need can live in this struct
type templateBag struct{}

func newTemplateFuncs() template.FuncMap {
	b := templateBag{}
	return template.FuncMap{
		"text": b.text,
	}
}

func (b templateBag) text(lang language.Tag, s string, args ...interface{}) string {
	return i18n.Text(lang, s, args...)
}

type Templated struct {
	t      *template.Template
	loader func() *template.Template
}

func Templates(paths ...string) Templated {
	h := Templated{
		loader: func() *template.Template {
			return template.Must(template.New("").Funcs(newTemplateFuncs()).ParseFiles(paths...))
		},
	}
	h.LoadTemplate()
	return h
}

func (t *Templated) LoadTemplate() {
	t.t = (t.loader)()
}

var _ httputil.TemplateLoader = (*Templated)(nil)