package api import ( "errors" "net/http" "strings" "time" _ "github.com/FernandoJVideira/simple_bank/docs" "github.com/FernandoJVideira/simple_bank/internal/auth" db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc" "github.com/FernandoJVideira/simple_bank/internal/handlers/http/response" "github.com/FernandoJVideira/simple_bank/internal/service" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type createUserRequest struct { Username string `json:"username" binding:"required,alphanum"` Password string `json:"password" binding:"required,min=6"` FullName string `json:"full_name" binding:"required"` Email string `json:"email" binding:"required,email"` } type userResponse struct { Username string `json:"username"` FullName string `json:"full_name"` Email string `json:"email"` PasswordChangedAt time.Time `json:"password_changed_at"` CreatedAt time.Time `json:"created_at"` } func newUserResponse(user db.User) userResponse { return userResponse{ Username: user.Username, FullName: user.FullName, Email: user.Email, PasswordChangedAt: user.PasswordChangedAt, CreatedAt: user.CreatedAt, } } // CreateUser godoc // @Summary Create a new user // @Description Register a new user account // @Tags auth // @Accept json // @Produce json // @Param request body createUserRequest true "User registration details" // @Success 201 {object} response.JSONResponse{data=userResponse} // @Failure 400 {object} response.JSONResponse // @Failure 500 {object} response.JSONResponse // @Router /v1/auth/register [post] func (s *Server) createUser(ctx *gin.Context) { var req createUserRequest var resp response.JSONResponse if err := ctx.ShouldBindJSON(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } result, err := s.userService.CreateUser(ctx, service.RegisterUserRequest{ Username: req.Username, Password: req.Password, FullName: req.FullName, Email: req.Email, }) if err != nil { errMessage := err.Error() if db.ErrorCode(err) == db.UniqueViolation { resp = response.JSONResponse{ Success: false, Error: errMessage, } ctx.JSON(http.StatusForbidden, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "User created successfully", Data: newUserResponse(result.User), } ctx.JSON(http.StatusCreated, resp) } type getUserRequest struct { Username string `uri:"username" binding:"required,alphanum"` } func (s *Server) getUser(ctx *gin.Context) { var req getUserRequest var resp response.JSONResponse if err := ctx.ShouldBindUri(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } user, err := s.userService.GetUser(ctx, req.Username) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { resp = response.JSONResponse{ Success: false, Error: "User not found", } ctx.JSON(http.StatusNotFound, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "User retrieved successfully", Data: newUserResponse(*user), } ctx.JSON(http.StatusOK, resp) } func (s *Server) getCurrentUser(ctx *gin.Context) { var resp response.JSONResponse authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload) user, err := s.userService.GetUser(ctx, authPayload.Username) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { resp = response.JSONResponse{ Success: false, Error: "User not found", } ctx.JSON(http.StatusNotFound, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "Current user retrieved successfully", Data: newUserResponse(*user), } ctx.JSON(http.StatusOK, resp) } type UpdateUserRequest struct { Username string `json:"username" binding:"required,alphanum"` FullName string `json:"full_name" binding:"omitempty,min=3"` Email string `json:"email" binding:"omitempty,email"` } func (s *Server) updateUser(ctx *gin.Context) { var req UpdateUserRequest var resp response.JSONResponse authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload) if err := ctx.ShouldBindJSON(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } if authPayload.Username != req.Username { resp = response.JSONResponse{ Success: false, Error: "Cannot update other user's information", } ctx.JSON(http.StatusForbidden, resp) return } params := service.UserUpdateRequest{ Username: strings.TrimSpace(req.Username), } if strings.TrimSpace(req.FullName) != "" { params.FullName = &req.FullName } if strings.TrimSpace(req.Email) != "" { params.Email = &req.Email } user, err := s.userService.UpdateUser(ctx, params) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { resp = response.JSONResponse{ Success: false, Error: "User not found", } ctx.JSON(http.StatusNotFound, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "User updated successfully", Data: newUserResponse(*user), } ctx.JSON(http.StatusOK, resp) } type changePasswordRequest struct { CurrentPassword string `json:"current_password" binding:"required,min=6"` NewPassword string `json:"new_password" binding:"required,min=6"` ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=NewPassword"` } func (s *Server) changePassword(ctx *gin.Context) { var req changePasswordRequest var resp response.JSONResponse authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload) if err := ctx.ShouldBindJSON(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } updUser, err := s.userService.UpdatePassword(ctx, service.PasswordUpdateRequest{ Username: authPayload.Username, OldPassword: req.CurrentPassword, NewPassword: req.NewPassword, }) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { resp = response.JSONResponse{ Success: false, Error: "User not found", } ctx.JSON(http.StatusNotFound, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "Password changed successfully", Data: newUserResponse(*updUser), } ctx.JSON(http.StatusOK, resp) } type loginUserRequest struct { Username string `json:"username" binding:"required,alphanum"` Password string `json:"password" binding:"required,min=6"` } type loginUserResponse struct { SessionID uuid.UUID `json:"session_id"` AccessToken string `json:"access_token"` AccessTokenExpiresAt time.Time `json:"access_token_expires_at"` RefreshToken string `json:"refresh_token"` RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"` User userResponse `json:"user"` } func (s *Server) loginUser(ctx *gin.Context) { var req loginUserRequest var resp response.JSONResponse if err := ctx.ShouldBindJSON(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } loginRes, err := s.authService.Login(ctx, service.LoginCredentials{ Username: req.Username, Password: req.Password, UserAgent: ctx.Request.UserAgent(), ClientIP: ctx.ClientIP(), }) if err != nil { statusCode := http.StatusInternalServerError errMsg := err.Error() // Map service errors to HTTP codes if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "user not found") { statusCode = http.StatusNotFound } else if strings.Contains(errMsg, "incorrect username or password") { statusCode = http.StatusUnauthorized } resp = response.JSONResponse{ Success: false, Error: errMsg, } ctx.JSON(statusCode, resp) return } resp = response.JSONResponse{ Success: true, Message: "User logged in successfully", Data: loginUserResponse{ SessionID: loginRes.SessionID, AccessToken: loginRes.AccessToken, AccessTokenExpiresAt: loginRes.AccessTokenExpiresAt, RefreshToken: loginRes.RefreshToken, RefreshTokenExpiresAt: loginRes.RefreshTokenExpiresAt, User: newUserResponse(loginRes.User), }, } ctx.JSON(http.StatusOK, resp) } type verifyEmailRequest struct { ID int64 `form:"id" binding:"required,min=1"` Code string `form:"code" binding:"required,min=32,max=128"` } type verifyEmailResponse struct { IsVerified bool `json:"is_verified"` } func (s *Server) verifyEmail(ctx *gin.Context) { var req verifyEmailRequest var resp response.JSONResponse if err := ctx.ShouldBindQuery(&req); err != nil { resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusBadRequest, resp) return } result, err := s.userService.VerifyEmail(ctx, service.EmailVerificationRequest{ EmailID: req.ID, SecretCode: req.Code, }) if err != nil { if errors.Is(err, db.ErrForeignKeyViolation) { resp = response.JSONResponse{ Success: false, Error: "Email verification record not found", } ctx.JSON(http.StatusForbidden, resp) return } if errors.Is(err, db.ErrCheckViolation) { resp = response.JSONResponse{ Success: false, Error: "Invalid verification code", } ctx.JSON(http.StatusBadRequest, resp) return } resp = response.JSONResponse{ Success: false, Error: err.Error(), } ctx.JSON(http.StatusInternalServerError, resp) return } resp = response.JSONResponse{ Success: true, Message: "Email verified successfully", Data: verifyEmailResponse{ IsVerified: result.User.IsEmailVerified, }, } ctx.JSON(http.StatusOK, resp) }