From caa9f09cbe0f46165cc3659deb10a34f8518c03a Mon Sep 17 00:00:00 2001 From: Toni Defez Date: Thu, 17 Apr 2025 12:52:06 +0200 Subject: [PATCH 1/2] feat: implement session management and flash messaging for snippets --- cmd/web/handlers.go | 9 ++++++--- cmd/web/helper.go | 12 ++++++++++-- cmd/web/main.go | 28 ++++++++++++++++++++-------- cmd/web/templates.go | 1 + docker/mysql/docker/mysql/init.sql | 5 +++++ go.mod | 2 ++ go.sum | 5 +++++ ui/html/base.tmpl | 4 ++++ 8 files changed, 53 insertions(+), 13 deletions(-) diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 3ed835d..d5fdec1 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -57,9 +57,10 @@ func (app *Application) SnippetView(w http.ResponseWriter, r *http.Request) { return } - app.render(w, r, http.StatusOK, "view.tmpl", templateData{ - Snippet: snippet, - }) + data := app.NewTemplateData(r) + data.Snippet = snippet + + app.render(w, r, http.StatusOK, "view.tmpl", *data) } @@ -122,6 +123,8 @@ func (app *Application) SnippetCreatePost(w http.ResponseWriter, r *http.Request app.serverError(w, r, err) return } + // Saving message flash + app.sessionManager.Put(r.Context(), "flash", "Snippet created successfully!") http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) } diff --git a/cmd/web/helper.go b/cmd/web/helper.go index 307c040..7ea88bd 100644 --- a/cmd/web/helper.go +++ b/cmd/web/helper.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "runtime/debug" + "time" "snippetbox.tonidefez.net/internal/models" ) @@ -54,7 +55,14 @@ func (app *Application) render(w http.ResponseWriter, r *http.Request, status in func (app *Application) NewTemplateData(r *http.Request) *templateData { return &templateData{ - Snippet: models.Snippet{}, - Snippets: []models.Snippet{}, + Snippet: models.Snippet{}, + Snippets: []models.Snippet{}, + CurrentYear: time.Now().Year(), + // 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. + + Flash: app.sessionManager.PopString(r.Context(), "flash"), } } diff --git a/cmd/web/main.go b/cmd/web/main.go index 5a22209..d68540f 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -7,7 +7,10 @@ import ( "net/http" "os" "text/template" + "time" + "github.com/alexedwards/scs/mysqlstore" + "github.com/alexedwards/scs/v2" _ "github.com/go-sql-driver/mysql" "snippetbox.tonidefez.net/internal/models" ) @@ -16,10 +19,10 @@ import ( // web application. For now we'll only include the structured logger, but we'll // add more to this as the build progresses. type Application struct { - logger *slog.Logger - // implemeting an interfaz we can make with differents dependencies DIP - snippets models.SnippetModeler - templateCache map[string]*template.Template + logger *slog.Logger + snippets models.SnippetModeler + templateCache map[string]*template.Template + sessionManager *scs.SessionManager } func main() { @@ -43,6 +46,14 @@ func main() { // before the main() function exits. defer db.Close() + // Use the scs.New() function to initialize a new session manager. Then we + // configure it to use our MySQL database as the session store, and set a + // lifetime of 12 hours (so that sessions automatically expire 12 hours + // after first being created). + sessionManager := scs.New() + sessionManager.Store = mysqlstore.New(db) + sessionManager.Lifetime = 12 * time.Hour + // Initialize a new template cache... templateCache, err := newTemplateCache() if err != nil { @@ -51,9 +62,10 @@ func main() { } app := &Application{ - logger: logger, - snippets: &models.SnippetModel{DB: db}, - templateCache: templateCache, + logger: logger, + snippets: &models.SnippetModel{DB: db}, + templateCache: templateCache, + sessionManager: sessionManager, } logger.Info("starting server", "addr", *addr) @@ -61,7 +73,7 @@ func main() { // Because the err variable is now already declared in the code above, we need // to use the assignment operator = here, instead of the := 'declare and assign' // operator. - err = http.ListenAndServe(*addr, app.routes()) + err = http.ListenAndServe(*addr, app.sessionManager.LoadAndSave(app.routes())) logger.Error(err.Error()) os.Exit(1) } diff --git a/cmd/web/templates.go b/cmd/web/templates.go index 7cc669c..2a11111 100644 --- a/cmd/web/templates.go +++ b/cmd/web/templates.go @@ -17,6 +17,7 @@ type templateData struct { Snippet models.Snippet Snippets []models.Snippet Form any + Flash string } func newTemplateCache() (map[string]*template.Template, error) { diff --git a/docker/mysql/docker/mysql/init.sql b/docker/mysql/docker/mysql/init.sql index 5fefb9c..d2dad82 100644 --- a/docker/mysql/docker/mysql/init.sql +++ b/docker/mysql/docker/mysql/init.sql @@ -34,3 +34,8 @@ INSERT INTO snippets (title, content, created, expires) VALUES ( ); +CREATE TABLE sessions ( + token CHAR(43) PRIMARY KEY, + data BLOB NOT NULL, + expiry TIMESTAMP(6) NOT NULL +); \ No newline at end of file diff --git a/go.mod b/go.mod index 2406b07..3230f6e 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,7 @@ go 1.24.1 require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/alexedwards/scs/mysqlstore v0.0.0-20250417082927-ab20b3feb5e9 // indirect + github.com/alexedwards/scs/v2 v2.8.0 // indirect github.com/go-sql-driver/mysql v1.9.1 // indirect ) diff --git a/go.sum b/go.sum index 94796bd..4a1167c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/alexedwards/scs/mysqlstore v0.0.0-20250417082927-ab20b3feb5e9 h1:HsYYLdEqKkjHrnt77Tiu8hnD4TIswIa+czpnlJldIJs= +github.com/alexedwards/scs/mysqlstore v0.0.0-20250417082927-ab20b3feb5e9/go.mod h1:p8jK3D80sw1PFrCSdlcJF1O75bp55HqbgDyyCLM0FrE= +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-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= diff --git a/ui/html/base.tmpl b/ui/html/base.tmpl index ce037ea..1e02193 100644 --- a/ui/html/base.tmpl +++ b/ui/html/base.tmpl @@ -16,6 +16,10 @@ {{template "nav" .}}
+ + {{with .Flash}} +
{{.}}
+ {{end}} {{template "main" .}}
From 7f95d4576888f648e833bba4f0b2c7f73312d974 Mon Sep 17 00:00:00 2001 From: Toni Defez Date: Thu, 17 Apr 2025 13:07:13 +0200 Subject: [PATCH 2/2] feat: integrate session management into snippet handlers tests --- cmd/web/handlers_test.go | 49 +++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/cmd/web/handlers_test.go b/cmd/web/handlers_test.go index 878ea1c..bc42dd8 100644 --- a/cmd/web/handlers_test.go +++ b/cmd/web/handlers_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/alexedwards/scs/v2" + "github.com/alexedwards/scs/v2/memstore" "snippetbox.tonidefez.net/internal/models" ) @@ -85,13 +87,19 @@ func TestSnippetView(t *testing.T) { t.Fatal(err) } + // Create fake session manager + sessionManager := scs.New() + sessionManager.Store = memstore.New() + app := &Application{ - logger: dummyLogger, - snippets: &models.MockSnippetModel{}, - templateCache: templateCache, + logger: dummyLogger, + snippets: &models.MockSnippetModel{}, + templateCache: templateCache, + sessionManager: sessionManager, } - router := app.routes() + //Be sure to manage session for each route + router := app.sessionManager.LoadAndSave(app.routes()) router.ServeHTTP(rr, req) // verify status @@ -115,18 +123,22 @@ func TestSnippetCreateGet(t *testing.T) { dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil)) dummyDB := &models.SnippetModel{DB: nil} templateCache, err := newTemplateCache() + // Create fake session manager + sessionManager := scs.New() + sessionManager.Store = memstore.New() if err != nil { t.Fatal(err) } app := &Application{ - logger: dummyLogger, - snippets: dummyDB, - templateCache: templateCache, + logger: dummyLogger, + snippets: dummyDB, + templateCache: templateCache, + sessionManager: sessionManager, } - router := app.routes() + router := app.sessionManager.LoadAndSave(app.routes()) router.ServeHTTP(rr, req) // verify status @@ -144,13 +156,18 @@ func TestSnippetCreatePost(t *testing.T) { dummyLogger := slog.New(slog.NewTextHandler(io.Discard, nil)) + // Create fake session manager + sessionManager := scs.New() + sessionManager.Store = memstore.New() + // simulate dependencies app := &Application{ - logger: dummyLogger, - snippets: &models.MockSnippetModel{}, + logger: dummyLogger, + snippets: &models.MockSnippetModel{}, + sessionManager: sessionManager, } - router := app.routes() + router := app.sessionManager.LoadAndSave(app.routes()) router.ServeHTTP(rr, req) // Verificar redirección @@ -172,8 +189,14 @@ func TestSnippetCreatePost_InvalidData(t *testing.T) { if err != nil { t.Fatal(err) } + + // Create fake session manager + sessionManager := scs.New() + sessionManager.Store = memstore.New() + app := &Application{ - templateCache: templateCache, + templateCache: templateCache, + sessionManager: sessionManager, } form := url.Values{} @@ -186,7 +209,7 @@ func TestSnippetCreatePost_InvalidData(t *testing.T) { rr := httptest.NewRecorder() - app.SnippetCreatePost(rr, req) + app.sessionManager.LoadAndSave(http.HandlerFunc(app.SnippetCreatePost)).ServeHTTP(rr, req) res := rr.Result() defer res.Body.Close()