feat: token creation, email templace change, activate user endpoint
This commit is contained in:
@@ -24,7 +24,7 @@ func (app *application) routes() http.Handler {
|
||||
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)
|
||||
|
||||
router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
|
||||
// Wrap the router with the rateLimit() middleware.
|
||||
return app.recoverPanic(app.rateLimit(router))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"greenlight.debuggingjon.dev/internal/data"
|
||||
"greenlight.debuggingjon.dev/internal/validator"
|
||||
@@ -50,13 +51,9 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque
|
||||
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)
|
||||
@@ -66,20 +63,99 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Use the background helper to execute an anonymous function that sends the welcome
|
||||
// email.
|
||||
// After the user record has been created in the database, generate a new activation
|
||||
// token for the user.
|
||||
token, err := app.models.Tokens.New(user.ID, 3*24*time.Hour, data.ScopeActivation)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
app.background(func() {
|
||||
err = app.mailer.Send(user.Email, "user_welcome.tmpl", user)
|
||||
// As there are now multiple pieces of data that we want to pass to our email
|
||||
// templates, we create a map to act as a 'holding structure' for the data. This
|
||||
// contains the plaintext version of the activation token for the user, along
|
||||
// with their ID.
|
||||
data := map[string]any{
|
||||
"activationToken": token.Plaintext,
|
||||
"userID": user.ID,
|
||||
}
|
||||
|
||||
// Send the welcome email, passing in the map above as dynamic data.
|
||||
err = app.mailer.Send(user.Email, "user_welcome.tmpl", data)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) activateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the plaintext activation token from the request body.
|
||||
var input struct {
|
||||
TokenPlaintext string `json:"token"`
|
||||
}
|
||||
|
||||
err := app.readJSON(w, r, &input)
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the plaintext token provided by the client.
|
||||
v := validator.New()
|
||||
|
||||
if data.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the details of the user associated with the token using the
|
||||
// GetForToken() method (which we will create in a minute). If no matching record
|
||||
// is found, then we let the client know that the token they provided is not valid.
|
||||
user, err := app.models.Users.GetForToken(data.ScopeActivation, input.TokenPlaintext)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, data.ErrRecordNotFound):
|
||||
v.AddError("token", "invalid or expired activation token")
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
default:
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Update the user's activation status.
|
||||
user.Activated = true
|
||||
|
||||
// Save the updated user record in our database, checking for any edit conflicts in
|
||||
// the same way that we did for our movie records.
|
||||
err = app.models.Users.Update(user)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, data.ErrEditConflict):
|
||||
app.editConflictResponse(w, r)
|
||||
default:
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If everything went successfully, then we delete all activation tokens for the
|
||||
// user.
|
||||
err = app.models.Tokens.DeleteAllForUser(data.ScopeActivation, user.ID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the updated user details to the client in a JSON response.
|
||||
err = app.writeJSON(w, http.StatusOK, envelope{"user": user}, nil)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user