feat: mailer internal, templates, bruno cleanup, create user, send welcome mail, mailtrap

This commit is contained in:
2026-03-26 09:46:32 +01:00
parent 095f7aabeb
commit bedc010584
16 changed files with 311 additions and 4 deletions
+16
View File
@@ -175,3 +175,19 @@ func (app *application) readInt(qs url.Values, key string, defaultValue int, v *
// Otherwise, return the converted integer value.
return i
}
// The background() helper accepts an arbitrary function as a parameter.
func (app *application) background(fn func()) {
// Launch a background goroutine.
go func() {
// Recover any panic.
defer func() {
if err := recover(); err != nil {
app.logger.PrintError(fmt.Errorf("%s", err), nil)
}
}()
// Execute the arbitrary function that we passed as the parameter.
fn()
}()
}
+20
View File
@@ -11,6 +11,7 @@ import (
_ "github.com/lib/pq"
"greenlight.debuggingjon.dev/internal/data"
"greenlight.debuggingjon.dev/internal/jsonlog"
"greenlight.debuggingjon.dev/internal/mailer"
)
// Declare a string containing the application version number. Later in the book we'll
@@ -39,6 +40,13 @@ type config struct {
burst int
enabled bool
}
smtp struct {
host string
port int
username string
password string
sender string
}
}
// Define an application struct to hold the dependencies for our HTTP handlers, helpers,
@@ -48,6 +56,7 @@ type application struct {
config config
logger *jsonlog.Logger
models data.Models
mailer mailer.Mailer
}
func main() {
@@ -80,6 +89,16 @@ func main() {
flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst")
flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter")
// Read the SMTP server configuration settings into the config struct, using the
// Mailtrap settings as the default values. IMPORTANT: If you're following along,
// make sure to replace the default values for smtp-username and smtp-password
// with your own Mailtrap credentials.
flag.StringVar(&cfg.smtp.host, "smtp-host", "sandbox.smtp.mailtrap.io", "SMTP host")
flag.IntVar(&cfg.smtp.port, "smtp-port", 25, "SMTP port")
flag.StringVar(&cfg.smtp.username, "smtp-username", "3d946287da35ea", "SMTP username")
flag.StringVar(&cfg.smtp.password, "smtp-password", "d06a774b484ca3", "SMTP password")
flag.StringVar(&cfg.smtp.sender, "smtp-sender", "Greenlight <no-reply@greenlight.debuggingjon.dev>", "SMTP sender")
flag.Parse()
// Call the openDB() helper function (see below) to create the connection pool,
@@ -105,6 +124,7 @@ func main() {
config: cfg,
logger: logger,
models: data.NewModels(db),
mailer: mailer.New(cfg.smtp.host, cfg.smtp.port, cfg.smtp.username, cfg.smtp.password, cfg.smtp.sender),
}
err = app.serve()
+1
View File
@@ -23,6 +23,7 @@ func (app *application) routes() http.Handler {
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
// Wrap the router with the rateLimit() middleware.
return app.recoverPanic(app.rateLimit(router))
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"errors"
"net/http"
"greenlight.debuggingjon.dev/internal/data"
"greenlight.debuggingjon.dev/internal/validator"
)
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
// Create an anonymous struct to hold the expected data from the request body.
var input struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
// Parse the request body into the anonymous struct.
err := app.readJSON(w, r, &input)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
// Copy the data from the request body into a new User struct. Notice also that we
// set the Activated field to false, which isn't strictly necessary because the
// Activated field will have the zero-value of false by default. But setting this
// explicitly helps to make our intentions clear to anyone reading the code.
user := &data.User{
Name: input.Name,
Email: input.Email,
Activated: false,
}
// Use the Password.Set() method to generate and store the hashed and plaintext
// passwords.
err = user.Password.Set(input.Password)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
v := validator.New()
// Validate the user struct and return the error messages to the client if any of
// the checks fail.
if data.ValidateUser(v, user); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
// Insert the user data into the database.
err = app.models.Users.Insert(user)
if err != nil {
switch {
// If we get a ErrDuplicateEmail error, use the v.AddError() method to manually
// add a message to the validator instance, and then call our
// failedValidationResponse() helper.
case errors.Is(err, data.ErrDuplicateEmail):
v.AddError("email", "a user with this email address already exists")
app.failedValidationResponse(w, r, v.Errors)
default:
app.serverErrorResponse(w, r, err)
}
return
}
// Use the background helper to execute an anonymous function that sends the welcome
// email.
app.background(func() {
err = app.mailer.Send(user.Email, "user_welcome.tmpl", user)
if err != nil {
app.logger.PrintError(err, nil)
}
})
// Note that we also change this to send the client a 202 Accepted status code.
// This status code indicates that the request has been accepted for processing, but
// the processing has not been completed.
err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}