Compare commits

..

2 Commits

Author SHA1 Message Date
lbenedar
b50cd42f4f ch9.3 2026-03-05 17:46:26 +03:00
lbenedar
c2883b9916 ch8.5 2026-03-05 15:59:44 +03:00
9 changed files with 122 additions and 47 deletions

View File

@@ -6,18 +6,17 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"unicode/utf8"
"gitea.local.lab/Lbenedar/snippetbox/internal/models"
"gitea.local.lab/Lbenedar/snippetbox/internal/validator"
"github.com/julienschmidt/httprouter"
)
type snippetCreateForm struct {
Title string
Content string
Expires int
FieldErrors map[string]string
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) {
@@ -89,40 +88,19 @@ 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.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")
form.CheckField(validator.PermittedInt(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 and 365")
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
FieldErrors: map[string]string{},
}
if strings.TrimSpace(form.Title) == "" {
form.FieldErrors["title"] = "This field cannot be blank"
} else if utf8.RuneCountInString(form.Title) > 100 {
form.FieldErrors["title"] = "This field cannot be more than 100 character long"
}
if strings.TrimSpace(form.Content) == "" {
form.FieldErrors["content"] = "This field cannot be blank"
}
if form.Expires != 1 && form.Expires != 7 && form.Expires != 365 {
form.FieldErrors["expires"] = "This field must equal 1, 7 and 365"
}
if len(form.FieldErrors) > 0 {
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
@@ -135,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)
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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"),
}
}

3
go.mod
View File

@@ -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

7
go.sum
View File

@@ -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=

View File

@@ -0,0 +1,46 @@
package validator
import (
"strings"
"unicode/utf8"
)
type Validator struct {
FieldErrors map[string]string
}
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
}
func (v *Validator) AddFieldError(key, message string) {
if v.FieldErrors == nil {
v.FieldErrors = make(map[string]string)
}
if _, exist := v.FieldErrors[key]; !exist {
v.FieldErrors[key] = message
}
}
func (v *Validator) CheckField(ok bool, key, message string) {
if !ok {
v.AddFieldError(key, message)
}
}
func NotBlank(value string) bool {
return strings.TrimSpace(value) != ""
}
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
func PermittedInt(value int, permittedValues ...int) bool {
for i := range permittedValues {
if value == permittedValues[i] {
return true
}
}
return false
}

View File

@@ -14,6 +14,9 @@
</header>
{{template "nav" .}}
<main>
{{with .Flash}}
<div class='flash'>{{.}}</div>
{{end}}
{{template "main" .}}
</main>
<footer>