|
|
// test
|
|
|
package server
|
|
|
|
|
|
import (
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"log/slog"
|
|
|
"net/http"
|
|
|
"strconv"
|
|
|
|
|
|
"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, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
|
|
|
return http.HandlerFunc(
|
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
|
// Retrieve Snippets from DB
|
|
|
snippets, err := snippetService.Lastest()
|
|
|
if err != err {
|
|
|
serverError(w, r, err)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
logger.Debug(fmt.Sprintf("%d snippets retrieved", len(snippets)))
|
|
|
|
|
|
// Old way. We want default data so
|
|
|
// data := templateData{
|
|
|
// Snippets: snippets,
|
|
|
// }
|
|
|
data := newTemplateData(r, sm)
|
|
|
data.Snippets = snippets
|
|
|
|
|
|
renderTemplate(w, r, tc, http.StatusOK, "home.go.tmpl", data)
|
|
|
|
|
|
// // Initialize a slice containing the paths to the two files. It's important
|
|
|
// // to note that the file containing our base template must be the *first*
|
|
|
// // file in the slice.
|
|
|
// files := []string{
|
|
|
// "./ui/html/base.go.tmpl",
|
|
|
// "./ui/html/partials/nav.go.tmpl",
|
|
|
// "./ui/html/pages/home.go.tmpl",
|
|
|
// }
|
|
|
|
|
|
// read template file into template set.
|
|
|
// ts, err := template.ParseFiles(files...)
|
|
|
// ts, err := parseTemplateFiles(files...)
|
|
|
// if err != nil {
|
|
|
// serverError(w, r, err)
|
|
|
// return
|
|
|
// }
|
|
|
// // Write template content to response body
|
|
|
// err = ts.ExecuteTemplate(w, "base", data)
|
|
|
// if err != nil {
|
|
|
// // This is the older more verbose way of doing what RatchetServer.serverError does
|
|
|
// // rs.logger.Error(err.Error())
|
|
|
// // http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
// serverError(w, r, err)
|
|
|
// }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
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"))
|
|
|
if err != nil || id < 1 {
|
|
|
clientError(w, http.StatusNotFound)
|
|
|
return
|
|
|
}
|
|
|
// Set a new cache-control header. If an existing "Cache-Control" header exists
|
|
|
// it will be overwritten.
|
|
|
// w.Header().Set("Cache-Control", "public, max-age=31536000")
|
|
|
|
|
|
snippet, err := snippetService.Get(id)
|
|
|
if err != nil {
|
|
|
logger.Debug(fmt.Sprintf("Failed to retrieve an active record with id: %d", id))
|
|
|
if errors.Is(err, model.ErrNoRecord) {
|
|
|
clientError(w, http.StatusNotFound)
|
|
|
} else {
|
|
|
serverError(w, r, err)
|
|
|
}
|
|
|
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",
|
|
|
// "./ui/html/pages/view.go.tmpl",
|
|
|
// }
|
|
|
|
|
|
// //ts, err := template.ParseFiles(files...)
|
|
|
// ts, err := parseTemplateFiles(files...)
|
|
|
// if err != nil {
|
|
|
// serverError(w, r, err)
|
|
|
// return
|
|
|
// }
|
|
|
|
|
|
// data := templateData{
|
|
|
// Snippet: snippet,
|
|
|
// }
|
|
|
|
|
|
// logger.Debug(fmt.Sprintf("created template: %s", ts.Name()))
|
|
|
|
|
|
// Any data that you pass as the final parameter to ts.ExecuteTemplate()
|
|
|
// is represented within your HTML templates by the . character (referred to as dot).
|
|
|
// In this specific case, the underlying type of dot will be a models.Snippet struct.
|
|
|
// When the underlying type of dot is a struct, you can render (or yield) the value
|
|
|
// of any exported field in your templates by postfixing dot with the field name
|
|
|
// field, we could yield the snippet title by writing {{.Title}} in our templates.
|
|
|
// err = ts.ExecuteTemplate(w, "base", data)
|
|
|
// if err != nil {
|
|
|
// serverError(w, r, err)
|
|
|
// }
|
|
|
// data := templateData{
|
|
|
// Snippet: snippet,
|
|
|
// }
|
|
|
data := newTemplateData(r, sm)
|
|
|
data.Snippet = snippet
|
|
|
// data.Flash = flash
|
|
|
renderTemplate(w, r, tc, http.StatusOK, "view.go.tmpl", data)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
func handleSnippetCreateGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler {
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
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
|
|
|
// snippet expiry to 365 days.
|
|
|
data.Form = snippetCreateForm{
|
|
|
Expires: 365,
|
|
|
}
|
|
|
renderTemplate(w, r, tc, http.StatusOK, "create.go.tmpl", data)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 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, 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
|
|
|
// or they will fail to take effect.
|
|
|
w.Header().Add("Server", "Dirp")
|
|
|
|
|
|
// Limit the request body size to 4096 bytes
|
|
|
// by default it's 10mb unless enctype="multipart/form-data in which case
|
|
|
// thier is no limit. You would do this however to limit the number of bytes
|
|
|
// that r.ParseForm will process. In this case it is only the first 4096 bytes
|
|
|
// reading beyond that will cause the MaxBytesReader to throw and error surfaced
|
|
|
// in your call to r.ParseForm()
|
|
|
// Also if the limit is reached the reader flags the server which will terminate
|
|
|
// the under lying TCP connection.
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, 4096)
|
|
|
|
|
|
// ParseForm is idempotent. Can be called many times.
|
|
|
// First we call r.ParseForm() which adds any data in POST request bodies
|
|
|
// to the r.PostForm map. This also works in the same way for PUT and PATCH
|
|
|
// requests. If there are any errors, we use our app.ClientError() helper to
|
|
|
// send a 400 Bad Request response to the user.
|
|
|
|
|
|
// If you have a form that submits data using the HTTP method GET, rather than POST,
|
|
|
// the form data will be included as URL query string parameters /foo/bar?title=value&content=value.
|
|
|
// can be retrieved using r.URL.Query().Get() which will return the value or an empty string.
|
|
|
// r.Form could be used, but it is clearer and more explicit to stick with r.URL.Query and r.PostForm
|
|
|
// to access submitted data.
|
|
|
err := r.ParseForm()
|
|
|
if err != nil {
|
|
|
clientError(w, http.StatusBadRequest)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// initializes the struct with zero values. So &form is non-nil
|
|
|
var form snippetCreateForm
|
|
|
|
|
|
// AUTOMATIC FORM PROCESSING
|
|
|
// When we call app.formDecoder.Decode() it requires a non-nil pointer as the target
|
|
|
// decode destination. If we try to pass in something that isn’t a non-nil pointer, then
|
|
|
// Decode() will return a form.InvalidDecoderError error. That error should be handled
|
|
|
// differently form a 400 error since it's a server side problem (ie 5xx error)
|
|
|
err = decodePostForm(r, formDecoder, &form)
|
|
|
|
|
|
if err != nil {
|
|
|
clientError(w, http.StatusBadRequest)
|
|
|
return
|
|
|
}
|
|
|
// OLD WAY
|
|
|
// The r.PostForm.Get() method always returns the form data as a *string*.
|
|
|
// However, we're expecting our expires value to be a number, and want to
|
|
|
// represent it in our Go code as an integer. So we need to manually convert
|
|
|
// the form data to an integer using strconv.Atoi(), and we send a 400 Bad
|
|
|
// Request response if the conversion fails.
|
|
|
// expires, err := strconv.Atoi(r.PostForm.Get("expires"))
|
|
|
// if err != nil {
|
|
|
// clientError(w, http.StatusBadRequest)
|
|
|
// return
|
|
|
|
|
|
// }
|
|
|
|
|
|
// Create an instance of the snippetCreateForm struct containing the values
|
|
|
// from the form and an empty map for any validation errors.
|
|
|
// form := snippetCreateForm{
|
|
|
// Title: r.PostForm.Get("title"),
|
|
|
// Content: r.PostForm.Get("content"),
|
|
|
// Expires: expires,
|
|
|
// }
|
|
|
|
|
|
// VALIDATION
|
|
|
// THE OLD WAY
|
|
|
// if strings.TrimSpace(form.Title) == "" {
|
|
|
// form.FieldErrors["title"] = "field cannot be blank"
|
|
|
// // we want to count the number of unicode code points not bytes in the string
|
|
|
// } else if utf8.RuneCountInString(form.Title) > 100 {
|
|
|
// form.FieldErrors["title"] = "This field cannot exceed 100 characters"
|
|
|
// }
|
|
|
// if strings.TrimSpace(form.Content) == "" {
|
|
|
// form.FieldErrors["content"] = "field cannot be blank"
|
|
|
// }
|
|
|
// if expires != 1 && expires != 7 && expires != 365 {
|
|
|
// form.FieldErrors["expires"] = "This field must equal 1,7, or 365"
|
|
|
// }
|
|
|
|
|
|
// If there are any validation errors, then re-display the create.tmpl template,
|
|
|
// passing in the snippetCreateForm instance as dynamic data in the Form
|
|
|
// field. Note that we use the HTTP status code 422 Unprocessable Entity
|
|
|
// when sending the response to indicate that there was a validation error.
|
|
|
// if len(form.FieldErrors) > 0 {
|
|
|
// data := newTemplateData()
|
|
|
// data.Form = form
|
|
|
// renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data)
|
|
|
// return
|
|
|
// }
|
|
|
|
|
|
// New VALIDATION
|
|
|
form.CheckField(validator.NotBlank(form.Title), "title", "this field cannot be blank")
|
|
|
form.CheckField(validator.MaxChars(form.Title, 100), "title", "this field cannot exceed 100 characters")
|
|
|
form.CheckField(validator.NotBlank(form.Content), "content", "this field cannot be blank")
|
|
|
form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "this field cannot be blank")
|
|
|
|
|
|
if !form.Valid() {
|
|
|
data := newTemplateData(r, sm)
|
|
|
data.Form = form
|
|
|
renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// NOTE
|
|
|
//
|
|
|
// In the case of a form field with multiple values you’ll need to work with the
|
|
|
// r.PostForm map directly. The underlying type of the r.PostForm map is url.Values,
|
|
|
// which in turn has the underlying type map[string][]string. So, for fields with
|
|
|
// multiple values you can loop over the underlying map to access them like so:
|
|
|
// for i, item := range r.PostForm["items"] {
|
|
|
// fmt.Fprintf(w, "%d: Item %s\n", i, item)
|
|
|
// }
|
|
|
|
|
|
id, err := snippetService.Insert(form.Title, form.Content, form.Expires)
|
|
|
if err != nil {
|
|
|
serverError(w, r, err)
|
|
|
}
|
|
|
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)
|
|
|
})
|
|
|
}
|