Files
LK_API_Temp/cmd/api/auth.go
T

379 lines
9.7 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"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"`
}
type ForgotPasswordPayload struct {
Email string `json:"email" validate:"required,email,max=255"`
}
type AuthResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refresh_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/register [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.inviteExp)
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
}
//* send the token back to the user
authResponse, err := api.createTokens(user)
if err != nil {
api.internalServerError(w, r, err)
return
}
if err := api.jsonResponse(w, http.StatusCreated, authResponse); err != nil {
api.internalServerError(w, r, err)
return
}
}
// RefreshTokenHandler godoc
//
// @Summary Refresh a token
// @Description Refresh a token for the user
// @Tags authentication
// @Accept json
// @Produce json
// @Param Authorization header string true "Refresh Token"
// @Success 201 {string} string "Token Created"
// @Failure 400 {object} error
// @Failure 401 {object} error
// @Failure 500 {object} error
// @Router /authentication/refresh [post]
func (api *api) refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
//* parse the refresh token from the request
refreshHeader := r.Header.Get("Authorization")
if refreshHeader == "" {
api.unauthorizedError(w, r, errors.New("refresh token is required"))
return
}
parts := strings.Split(refreshHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
api.unauthorizedError(w, r, fmt.Errorf("invalid Authorization header"))
return
}
refreshToken := parts[1]
token, err := api.authenticator.ValidateRefreshToken(refreshToken)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
claims := token.Claims.(jwt.MapClaims)
//* get the user id from the token
userID, err := strconv.ParseInt(fmt.Sprintf("%.f", claims["sub"]), 10, 64)
if err != nil {
api.unauthorizedError(w, r, fmt.Errorf("invalid token"))
return
}
//* get the user from the database
user, err := api.store.Users.GetByID(r.Context(), userID)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
authResponse, err := api.createTokens(user)
if err != nil {
api.unauthorizedError(w, r, err)
return
}
if err := api.jsonResponse(w, http.StatusCreated, authResponse); err != nil {
api.internalServerError(w, r, err)
return
}
}
// ResetPasswordHandler godoc
//
// @Summary Forgot Password
// @Description Forgot Password
// @Tags authentication
// @Accept json
// @Produce json
// @Param payload body ForgotPasswordPayload true "User Credentials"
// @Success 201 {string} string "Token Created"
// @Failure 400 {object} error
// @Failure 401 {object} error
// @Failure 500 {object} error
// @Router /authentication/forgot-password [post]
func (api *api) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) {
var payload ForgotPasswordPayload
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, err := api.store.Users.GetByEmail(r.Context(), payload.Email)
if err != nil {
switch err {
case store.ErrNotFound:
api.badRequestError(w, r, err)
default:
api.internalServerError(w, r, err)
}
}
plainToken := uuid.New().String()
hash := sha256.Sum256([]byte(plainToken))
hashedToken := hex.EncodeToString(hash[:])
//*Store The Request
err = api.store.Users.PasswordResetRequest(r.Context(), user.ID, hashedToken, api.config.mail.passwordRefreshExp)
if err != nil {
api.internalServerError(w, r, err)
return
}
updateURL := fmt.Sprintf("%s/reset-password/%s", api.config.frontEndURL, plainToken)
isProdEnv := api.config.env == "production"
vars := struct {
Username string
ServiceName string
UpdateURL string
Email string
CurrentYear string
}{
Username: user.Username,
ServiceName: "LK_API_Temp",
UpdateURL: updateURL,
Email: user.Email,
CurrentYear: time.Now().Format("2006"),
}
status, err := api.mailer.Send(mailer.ForgotPasswordTemplate, user.Username, user.Email, vars, !isProdEnv)
if err != nil {
api.internalServerError(w, r, err)
return
}
api.logger.Infow("email sent", "status code", status)
if err := api.jsonResponse(w, http.StatusCreated, plainToken); err != nil {
api.internalServerError(w, r, err)
return
}
}
func (api *api) createTokens(user *store.User) (AuthResponse, error) {
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 {
return AuthResponse{}, err
}
refreshClaims := &jwt.MapClaims{
"sub": user.ID,
"exp": time.Now().Add(api.config.auth.token.refreshExp).Unix(),
}
log.Println(time.Now().Add(api.config.auth.token.refreshExp).Unix())
refreshToken, err := api.authenticator.GenerateRefreshToken(refreshClaims)
if err != nil {
return AuthResponse{}, err
}
return AuthResponse{
Token: token,
RefreshToken: refreshToken,
}, nil
}