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 }