Server side sessions

main
Drew Bednar 2 months ago
parent 244e291b77
commit 25f844d841

@ -6,10 +6,14 @@ import (
"log/slog"
"net/http"
"os"
"time"
rdb "git.runcible.io/learning/ratchet/internal/database"
"git.runcible.io/learning/ratchet/internal/logging"
"git.runcible.io/learning/ratchet/internal/server"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
_ "github.com/mattn/go-sqlite3"
// "git.runcible.io/learning/ratchet"
// ratchethttp "git.runcible.io/learning/ratchet/internal"
)
@ -43,11 +47,19 @@ func main() {
defer db.Close()
tc, err := server.InitTemplateCache()
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
// SessionManager
sm := scs.New()
sm.Store = sqlite3store.New(db)
sm.Lifetime = 12 * time.Hour
// Propagate build information to root package to share globally
// ratchet.Version = strings.TrimPrefix(version, "")
// ratchet.Commit = commit
server := server.NewRatchetServer(logger, tc, db)
server := server.NewRatchetServer(logger, tc, db, sm)
// START SERVING REQUESTS
slog.Debug("Herp dirp!")

@ -5,6 +5,7 @@ go 1.23.3
require github.com/mattn/go-sqlite3 v1.14.24
require (
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 // indirect
github.com/alexedwards/scs/v2 v2.8.0 // indirect
github.com/go-playground/form/v4 v4.2.1 // indirect
)

@ -1,7 +1,10 @@
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 h1:+DCxWg/ojncqS+TGAuRUoV7OfG/S4doh0pcpAwEcow0=
github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

@ -10,11 +10,12 @@ import (
"git.runcible.io/learning/ratchet/internal/model"
"git.runcible.io/learning/ratchet/internal/validator"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
)
// TODO function should accept and a pointer to an interface allowing for mocking in tests.
func handleHome(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler {
func handleHome(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Retrieve Snippets from DB
@ -30,7 +31,7 @@ func handleHome(logger *slog.Logger, tc *TemplateCache, snippetService *model.Sn
// data := templateData{
// Snippets: snippets,
// }
data := newTemplateData()
data := newTemplateData(r, sm)
data.Snippets = snippets
renderTemplate(w, r, tc, http.StatusOK, "home.go.tmpl", data)
@ -62,7 +63,7 @@ func handleHome(logger *slog.Logger, tc *TemplateCache, snippetService *model.Sn
})
}
func handleSnippetView(logger *slog.Logger, tc *TemplateCache, snippetService *model.SnippetService) http.Handler {
func handleSnippetView(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
@ -85,6 +86,15 @@ func handleSnippetView(logger *slog.Logger, tc *TemplateCache, snippetService *m
return
}
// Use the PopString() method to retrieve the value for the "flash" key.
// PopString() also deletes the key and value from the session data, so it
// acts like a one-time fetch. If there is no matching key in the session
// data this will return the empty string.
// See also GetInt, GetBool, GetBytes, GetTime etc.
// NOW DONE IN TEMPLATE DATA FUNC
// flash := sm.PopString(r.Context(), "flash")
// files := []string{
// "./ui/html/base.go.tmpl",
// "./ui/html/partials/nav.go.tmpl",
@ -117,15 +127,16 @@ func handleSnippetView(logger *slog.Logger, tc *TemplateCache, snippetService *m
// data := templateData{
// Snippet: snippet,
// }
data := newTemplateData()
data := newTemplateData(r, sm)
data.Snippet = snippet
data.Flash = flash
renderTemplate(w, r, tc, http.StatusOK, "view.go.tmpl", data)
})
}
func handleSnippetCreateGet(tc *TemplateCache) http.Handler {
func handleSnippetCreateGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := newTemplateData()
data := newTemplateData(r, sm)
// Initialize a new snippetCreateForm instance and pass it to the template.
// Notice how this is also a great opportunity to set any default or
// 'initial' values for the form --- here we set the initial value for the
@ -140,7 +151,7 @@ func handleSnippetCreateGet(tc *TemplateCache) http.Handler {
// snippetCreate handles display of the form used to create snippets
//
// curl -iL -d "" http://localhost:5001/snippet/create
func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder *form.Decoder, snippetService *model.SnippetService) http.Handler {
func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder *form.Decoder, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// example of a custom header. Must be done before calling WriteHeader
@ -242,7 +253,7 @@ func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder
form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "this field cannot be blank")
if !form.Valid() {
data := newTemplateData()
data := newTemplateData(r, sm)
data.Form = form
renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data)
return
@ -264,6 +275,10 @@ func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder
}
logger.Info(fmt.Sprintf("Inserted record. id: %d", id))
// Use the Put() method to add a string value ("Snippet successfully
// created!") and the corresponding key ("flash") to the session data.
sm.Put(r.Context(), "flash", "Snippet successfully created!")
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
})
}

@ -6,6 +6,7 @@ import (
"net/http"
"git.runcible.io/learning/ratchet/internal/model"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
)
@ -14,16 +15,17 @@ func addRoutes(mux *http.ServeMux,
tc *TemplateCache,
db *sql.DB,
fd *form.Decoder,
sm *scs.SessionManager,
snippetService *model.SnippetService) http.Handler {
// /{$} is used to prevent subtree path patterns from acting like a wildcard
// resulting in this route requiring an exact match on "/" only
// You can only include one HTTP method in a route pattern if you choose
// GET will match GET & HEAD http request methods
mux.Handle("GET /{$}", handleHome(logger, tc, snippetService))
mux.Handle("GET /snippet/view/{id}", handleSnippetView(logger, tc, snippetService))
mux.Handle("GET /snippet/create", handleSnippetCreateGet(tc))
mux.Handle("POST /snippet/create", handleSnippetCreatePost(logger, tc, fd, snippetService))
mux.Handle("GET /{$}", sm.LoadAndSave(handleHome(logger, tc, sm, snippetService))) // might be time to swith to github.com/justinas/alice dynamic chain
mux.Handle("GET /snippet/view/{id}", sm.LoadAndSave(handleSnippetView(logger, tc, sm, snippetService)))
mux.Handle("GET /snippet/create", sm.LoadAndSave(handleSnippetCreateGet(tc, sm)))
mux.Handle("POST /snippet/create", sm.LoadAndSave(handleSnippetCreatePost(logger, tc, fd, sm, snippetService)))
// mux.Handle("/something", handleSomething(logger, config))
// mux.Handle("/healthz", handleHealthzPlease(logger))
// mux.Handle("/", http.NotFoundHandler())

@ -6,6 +6,7 @@ import (
"net/http"
"git.runcible.io/learning/ratchet/internal/model"
"github.com/alexedwards/scs/v2"
"github.com/go-playground/form/v4"
)
@ -18,14 +19,16 @@ type RatchetServer struct {
snippetService *model.SnippetService
UserService model.UserService
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
func NewRatchetServer(logger *slog.Logger, tc *TemplateCache, db *sql.DB) *RatchetServer {
func NewRatchetServer(logger *slog.Logger, tc *TemplateCache, db *sql.DB, sm *scs.SessionManager) *RatchetServer {
rs := new(RatchetServer)
rs.logger = logger
rs.snippetService = &model.SnippetService{DB: db}
rs.formDecoder = form.NewDecoder()
rs.templateCache = tc
rs.sessionManager = sm
// TODO implement middleware that disables directory listings
fileServer := http.FileServer(http.Dir("./ui/static/"))
router := http.NewServeMux()
@ -35,7 +38,7 @@ func NewRatchetServer(logger *slog.Logger, tc *TemplateCache, db *sql.DB) *Ratch
// Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver.
// SEE we can really clean things up by moving this into routes.go and handlers.go
wrappedMux := addRoutes(router, rs.logger, rs.templateCache, db, rs.formDecoder, rs.snippetService)
wrappedMux := addRoutes(router, rs.logger, rs.templateCache, db, rs.formDecoder, sm, rs.snippetService)
rs.Handler = CommonHeaderMiddleware(wrappedMux)
rs.Handler = RequestLoggingMiddleware(rs.Handler, logger)
rs.Handler = RecoveryMiddleware(rs.Handler)

@ -10,6 +10,7 @@ import (
"time"
"git.runcible.io/learning/ratchet/internal/model"
"github.com/alexedwards/scs/v2"
)
// Define a templateData type to act as the holding structure for
@ -21,11 +22,12 @@ type templateData struct {
Snippet model.Snippet
Snippets []model.Snippet
Form any
Flash string
}
// newTemplateData is useful to inject default values. Example CSRF tokens for forms.
func newTemplateData() templateData {
return templateData{CurrentYear: time.Now().Year()}
func newTemplateData(r *http.Request, sm *scs.SessionManager) templateData {
return templateData{CurrentYear: time.Now().Year(), Flash: sm.PopString(r.Context(), "flash")}
}
// TEMPLATE FUNCTIONS

@ -15,6 +15,9 @@
</header>
{{template "nav" .}}
<main>
{{with .Flash}}
<div class='flash'>{{.}}</div>
{{end}}
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a> in {{ .CurrentYear }}</footer>

Loading…
Cancel
Save