diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 5410c5e..3d13c28 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -21,6 +21,7 @@ func (app *application) routes() http.Handler { 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) return app.recoverPanic(app.rateLimit(router)) } diff --git a/cmd/api/users.go b/cmd/api/users.go index abe100f..f840a49 100644 --- a/cmd/api/users.go +++ b/cmd/api/users.go @@ -75,3 +75,58 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque app.serverErrorResponse(w, r, err) } } + +func (app *application) activateUserHandler(w http.ResponseWriter, r *http.Request) { + var input struct { + TokenPlaintext string `json:"token"` + } + + err := app.readJSON(w, r, &input) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + + v := validator.New() + + if data.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + + 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 + } + + user.Activated = true + + 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 + } + + err = app.models.Tokens.DeleteForUser(data.ScopeActivation, user.ID) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + err = app.writeJSON(w, http.StatusOK, envelope{"user": user}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/internal/data/users.go b/internal/data/users.go index d203e98..eea41be 100644 --- a/internal/data/users.go +++ b/internal/data/users.go @@ -2,6 +2,7 @@ package data import ( "context" + "crypto/sha256" "database/sql" "errors" "time" @@ -169,3 +170,44 @@ func (m UserModel) Update(user *User) error { } return nil } + +func (m UserModel) GetForToken(tokenScope, tokenPlaintext string) (*User, error) { + tokenHash := sha256.Sum256([]byte(tokenPlaintext)) + + query := ` + SELECT users.id, users.created_at, users.name, users.email, users.password_hash, users.activated, users.version + FROM users + INNER JOIN tokens + ON users.id = tokens.user_id + WHERE tokens.hash = $1 + AND tokens.scope = $2 + AND tokens.expiry > $3` + + args := []any{tokenHash[:], tokenScope, time.Now()} + + var user User + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + err := m.DB.QueryRowContext(ctx, query, args...).Scan( + &user.ID, + &user.CreatedAt, + &user.Name, + &user.Email, + &user.Password.hash, + &user.Activated, + &user.Version, + ) + + if err != nil { + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, ErrRecordNotFound + default: + return nil, err + } + } + + return &user, nil +}