diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index 1f46db0..6b27d11 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -13,10 +13,10 @@ import ( ) type snippetCreateForm struct { - Title string - Content string - Expires int - validator.Validator + Title string `form:"title"` + Content string `form:"content"` + Expires int `form:"expires"` + validator.Validator `form:"-"` } func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) { @@ -88,24 +88,13 @@ func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) { } func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() + var form snippetCreateForm + err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } - expires, err := strconv.Atoi(r.PostForm.Get("expires")) - if err != nil { - app.clientError(w, http.StatusBadRequest) - return - } - - form := snippetCreateForm{ - Title: r.PostForm.Get("title"), - Content: r.PostForm.Get("content"), - Expires: expires, - } - form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 character long") form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") @@ -124,5 +113,7 @@ func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request return } + app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!") + http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) } diff --git a/cmd/web/helpers.go b/cmd/web/helpers.go index 37903e6..9c9ff5e 100644 --- a/cmd/web/helpers.go +++ b/cmd/web/helpers.go @@ -1,9 +1,12 @@ package main import ( + "errors" "fmt" "net/http" "runtime/debug" + + "github.com/go-playground/form/v4" ) func (app *application) serverError(w http.ResponseWriter, err error) { @@ -20,3 +23,20 @@ func (app *application) clientError(w http.ResponseWriter, status int) { func (app *application) notFound(w http.ResponseWriter) { app.clientError(w, http.StatusNotFound) } + +func (app *application) decodePostForm(r *http.Request, snippetForm *snippetCreateForm) error { + err := r.ParseForm() + if err != nil { + return err + } + + err = app.formDecoder.Decode(&snippetForm, r.PostForm) + if err != nil { + var invalidDecoderError *form.InvalidDecoderError + if errors.As(err, &invalidDecoderError) { + panic("Invalid decoder error") + } + return err + } + return nil +} diff --git a/cmd/web/main.go b/cmd/web/main.go index d80a947..5659352 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -7,17 +7,23 @@ import ( "log" "net/http" "os" + "time" "gitea.local.lab/Lbenedar/snippetbox/internal/models" + "github.com/alexedwards/scs/mysqlstore" + "github.com/alexedwards/scs/v2" + "github.com/go-playground/form/v4" _ "github.com/go-sql-driver/mysql" ) type application struct { - errorLog *log.Logger - infoLog *log.Logger - snippets *models.SnippetModel - templateCache map[string]*template.Template + errorLog *log.Logger + infoLog *log.Logger + snippets *models.SnippetModel + templateCache map[string]*template.Template + formDecoder *form.Decoder + sessionManager *scs.SessionManager } type config struct { @@ -47,11 +53,18 @@ func main() { errorLog.Fatal(err) } + formDecoder := form.NewDecoder() + sessionManager := scs.New() + sessionManager.Store = mysqlstore.New(db) + sessionManager.Lifetime = 12 * time.Hour + app := &application{ - errorLog: errorLog, - infoLog: infoLog, - snippets: &models.SnippetModel{DB: db}, - templateCache: templateCache, + errorLog: errorLog, + infoLog: infoLog, + snippets: &models.SnippetModel{DB: db}, + templateCache: templateCache, + formDecoder: formDecoder, + sessionManager: sessionManager, } srv := &http.Server{ diff --git a/cmd/web/routes.go b/cmd/web/routes.go index 73efb38..e98fc6e 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -17,12 +17,13 @@ func (app *application) routes() http.Handler { fileServer := http.FileServer(http.Dir("./ui/static/")) router.Handler(http.MethodGet, "/static/", http.StripPrefix("/static", fileServer)) - router.HandlerFunc(http.MethodGet, "/", app.home) - router.HandlerFunc(http.MethodGet, "/snippet/view/:id", app.snippetView) - router.HandlerFunc(http.MethodGet, "/snippet/create", app.snippetCreate) - router.HandlerFunc(http.MethodPost, "/snippet/create", app.snippetCreatePost) + dynamic := alice.New(app.sessionManager.LoadAndSave) + + router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home)) + router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView)) + router.Handler(http.MethodGet, "/snippet/create", dynamic.ThenFunc(app.snippetCreate)) + router.Handler(http.MethodPost, "/snippet/create", dynamic.ThenFunc(app.snippetCreatePost)) standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders) - return standard.Then(router) } diff --git a/cmd/web/templates.go b/cmd/web/templates.go index d58462c..d6275b9 100644 --- a/cmd/web/templates.go +++ b/cmd/web/templates.go @@ -14,6 +14,7 @@ type templateData struct { Snippet *models.Snippet Snippets []*models.Snippet Form any + Flash string } func humanDate(t time.Time) string { @@ -27,6 +28,7 @@ var functions = template.FuncMap{ func (app *application) newTemplateData(r *http.Request) *templateData { return &templateData{ CurrentYear: time.Now().Year(), + Flash: app.sessionManager.PopString(r.Context(), "flash"), } } diff --git a/go.mod b/go.mod index fea578c..b98643a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.25.0 require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/alexedwards/scs/mysqlstore v0.0.0-20251002162104-209de6e426de // indirect + github.com/alexedwards/scs/v2 v2.9.0 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/justinas/alice v1.2.0 // indirect diff --git a/go.sum b/go.sum index 09928e3..e2e115f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,12 @@ 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-20251002162104-209de6e426de h1:/Y/iIFgV1Ofvk4Euv5gUQ74vgqFZOQ1wlJQ3yz/zYGs= +github.com/alexedwards/scs/mysqlstore v0.0.0-20251002162104-209de6e426de/go.mod h1:p8jK3D80sw1PFrCSdlcJF1O75bp55HqbgDyyCLM0FrE= +github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= +github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= diff --git a/ui/html/base.tmpl b/ui/html/base.tmpl index 8344b94..27c7bbb 100644 --- a/ui/html/base.tmpl +++ b/ui/html/base.tmpl @@ -14,6 +14,9 @@ {{template "nav" .}}
+ {{with .Flash}} +
{{.}}
+ {{end}} {{template "main" .}}