feat: 0.0.1
This commit is contained in:
146 files changed
+10444
No files matched your search
@@ -0,0 +1,429 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user