Files

204 lines
5.2 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"time"
"github.com/FernandoVideira/LK_API_Temp/internal/mailer"
"github.com/FernandoVideira/LK_API_Temp/internal/store"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type RegisterUserPayload struct {
FirstName string `json:"first_name" validate:"required,max=100"`
LastName string `json:"last_name" validate:"required,max=100"`
Username string `json:"username" validate:"required,max=100"`
Email string `json:"email" validate:"required,email,max=255"`
Password string `json:"password" validate:"required,password"`
}
type UserWithToken struct {
*store.User
Token string `json:"token"`
}
// RegisterUser godoc
//
// @Summary Register a new User
// @Description Register a new User
// @Tags authentication
// @Accept json
// @Produce json
// @Param payload body RegisterUserPayload true "User Credentials"
// @Success 201 {object} UserWithToken "User Registered"
// @Failure 400 {object} error "User payload error"
// @Failure 500 {object} error "Internal Server Error"
// @Router /authentication/user [post]
func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) {
var payload RegisterUserPayload
if err := readJSON(w, r, &payload); err != nil {
api.badRequestError(w, r, err)
return
}
if err := Validate.Struct(payload); err != nil {
api.badRequestError(w, r, err)
return
}
user := &store.User{
FirstName: payload.FirstName,
LastName: payload.LastName,
Username: payload.Username,
Email: payload.Email,
Role: store.Role{
Name: "user",
},
}
// Hash the user password
if err := user.Password.Set(payload.Password); err != nil {
api.internalServerError(w, r, err)
return
}
ctx := r.Context()
plainToken := uuid.New().String()
hash := sha256.Sum256([]byte(plainToken))
hashedToken := hex.EncodeToString(hash[:])
// Store the user
err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.exp)
if err != nil {
switch {
case errors.Is(err, store.ErrDuplicateEmail):
api.badRequestError(w, r, err)
case errors.Is(err, store.ErrDuplicateUsername):
api.badRequestError(w, r, err)
default:
api.internalServerError(w, r, err)
}
return
}
userWithToken := UserWithToken{
User: user,
Token: plainToken,
}
activationURL := fmt.Sprintf("%s/confirm/%s", api.config.frontEndURL, plainToken)
isProdEnv := api.config.env == "production"
vars := struct {
ServiceName string
Username string
ActivationURL string
}{
ServiceName: "LK_API_Temp",
Username: user.Username,
ActivationURL: activationURL,
}
// Send Email
status, err := api.mailer.Send(mailer.UserWelcomeTemplate, user.Username, user.Email, vars, !isProdEnv)
if err != nil {
// Log the error
api.logger.Errorw("failed to send welcome email", "error", err)
//Rollback the user creation if the email fails
if err := api.store.Users.Delete(ctx, user.ID); err != nil {
api.logger.Errorw("failed to rollback user creation", "error", err)
}
api.internalServerError(w, r, err)
return
}
api.logger.Infow("email sent", "status code", status)
if err := api.jsonResponse(w, http.StatusCreated, userWithToken); err != nil {
api.internalServerError(w, r, err)
return
}
}
type CredentialsPayload struct {
Email string `json:"email" validate:"required,email,max=255"`
Password string `json:"password" validate:"required,min=3,max=72"`
}
// CreateTokenHandler godoc
//
// @Summary Create a new token
// @Description Create a new token for the user
// @Tags authentication
// @Accept json
// @Produce json
// @Param payload body CredentialsPayload true "User Credentials"
// @Success 201 {string} string "Token Created"
// @Failure 400 {object} error
// @Failure 401 {object} error
// @Failure 500 {object} error
// @Router /authentication/token [post]
func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) {
//* parse payload credentials
var payload CredentialsPayload
if err := readJSON(w, r, &payload); err != nil {
api.badRequestError(w, r, err)
return
}
if err := Validate.Struct(payload); err != nil {
api.badRequestError(w, r, err)
return
}
//* fetch the user (check if the user exists) from the payload
user, err := api.store.Users.GetByEmail(r.Context(), payload.Email)
if err != nil {
switch err {
case store.ErrNotFound:
api.unauthorizedError(w, r, err)
default:
api.internalServerError(w, r, err)
}
return
}
//* compare the password from the payload with the user password
if err := user.Password.Compare(payload.Password); err != nil {
api.unauthorizedError(w, r, err)
return
}
claims := &jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(api.config.auth.token.exp).Unix(),
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"iss": api.config.auth.token.iss,
"aud": api.config.auth.token.aud,
}
//* generate a new token -> add the claims
token, err := api.authenticator.GenerateToken(claims)
if err != nil {
api.internalServerError(w, r, err)
return
}
//* send the token back to the user
if err := api.jsonResponse(w, http.StatusCreated, token); err != nil {
api.internalServerError(w, r, err)
return
}
}