Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,14 +4,15 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||
"github.com/treytartt/casera-api/internal/dto/responses"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
"github.com/treytartt/casera-api/internal/validator"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication endpoints
|
||||
@@ -43,65 +44,38 @@ func (h *AuthHandler) SetGoogleAuthService(googleAuth *services.GoogleAuthServic
|
||||
}
|
||||
|
||||
// Login handles POST /api/auth/login/
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
func (h *AuthHandler) Login(c echo.Context) error {
|
||||
var req requests.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
response, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
status := http.StatusUnauthorized
|
||||
message := i18n.LocalizedMessage(c, "error.invalid_credentials")
|
||||
|
||||
if errors.Is(err, services.ErrUserInactive) {
|
||||
message = i18n.LocalizedMessage(c, "error.account_inactive")
|
||||
}
|
||||
|
||||
log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed")
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Register handles POST /api/auth/register/
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
func (h *AuthHandler) Register(c echo.Context) error {
|
||||
var req requests.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
response, confirmationCode, err := h.authService.Register(&req)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
message := err.Error()
|
||||
|
||||
if errors.Is(err, services.ErrUsernameTaken) {
|
||||
message = i18n.LocalizedMessage(c, "error.username_taken")
|
||||
} else if errors.Is(err, services.ErrEmailTaken) {
|
||||
message = i18n.LocalizedMessage(c, "error.email_taken")
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
message = i18n.LocalizedMessage(c, "error.registration_failed")
|
||||
log.Error().Err(err).Msg("Registration failed")
|
||||
}
|
||||
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
log.Debug().Err(err).Msg("Registration failed")
|
||||
return err
|
||||
}
|
||||
|
||||
// Send welcome email with confirmation code (async)
|
||||
@@ -113,15 +87,14 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// Logout handles POST /api/auth/logout/
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
func (h *AuthHandler) Logout(c echo.Context) error {
|
||||
token := middleware.GetAuthToken(c)
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.not_authenticated")})
|
||||
return
|
||||
return apperrors.Unauthorized("error.not_authenticated")
|
||||
}
|
||||
|
||||
// Invalidate token in database
|
||||
@@ -131,101 +104,73 @@ func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
|
||||
// Invalidate token in cache
|
||||
if h.cache != nil {
|
||||
if err := h.cache.InvalidateAuthToken(c.Request.Context(), token); err != nil {
|
||||
if err := h.cache.InvalidateAuthToken(c.Request().Context(), token); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to invalidate token in cache")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.logged_out")})
|
||||
return c.JSON(http.StatusOK, responses.MessageResponse{Message: "Logged out successfully"})
|
||||
}
|
||||
|
||||
// CurrentUser handles GET /api/auth/me/
|
||||
func (h *AuthHandler) CurrentUser(c *gin.Context) {
|
||||
user := middleware.MustGetAuthUser(c)
|
||||
if user == nil {
|
||||
return
|
||||
func (h *AuthHandler) CurrentUser(c echo.Context) error {
|
||||
user, err := middleware.MustGetAuthUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := h.authService.GetCurrentUser(user.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to get current user")
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_get_user")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UpdateProfile handles PUT/PATCH /api/auth/profile/
|
||||
func (h *AuthHandler) UpdateProfile(c *gin.Context) {
|
||||
user := middleware.MustGetAuthUser(c)
|
||||
if user == nil {
|
||||
return
|
||||
func (h *AuthHandler) UpdateProfile(c echo.Context) error {
|
||||
user, err := middleware.MustGetAuthUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req requests.UpdateProfileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
response, err := h.authService.UpdateProfile(user.ID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrEmailTaken) {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_taken")})
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile")
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_update_profile")})
|
||||
return
|
||||
log.Debug().Err(err).Uint("user_id", user.ID).Msg("Failed to update profile")
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// VerifyEmail handles POST /api/auth/verify-email/
|
||||
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
user := middleware.MustGetAuthUser(c)
|
||||
if user == nil {
|
||||
return
|
||||
func (h *AuthHandler) VerifyEmail(c echo.Context) error {
|
||||
user, err := middleware.MustGetAuthUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req requests.VerifyEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
err := h.authService.VerifyEmail(user.ID, req.Code)
|
||||
err = h.authService.VerifyEmail(user.ID, req.Code)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
message := err.Error()
|
||||
|
||||
if errors.Is(err, services.ErrInvalidCode) {
|
||||
message = i18n.LocalizedMessage(c, "error.invalid_verification_code")
|
||||
} else if errors.Is(err, services.ErrCodeExpired) {
|
||||
message = i18n.LocalizedMessage(c, "error.verification_code_expired")
|
||||
} else if errors.Is(err, services.ErrAlreadyVerified) {
|
||||
message = i18n.LocalizedMessage(c, "error.email_already_verified")
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
message = i18n.LocalizedMessage(c, "error.verification_failed")
|
||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Email verification failed")
|
||||
}
|
||||
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
log.Debug().Err(err).Uint("user_id", user.ID).Msg("Email verification failed")
|
||||
return err
|
||||
}
|
||||
|
||||
// Send post-verification welcome email with tips (async)
|
||||
@@ -237,29 +182,23 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.VerifyEmailResponse{
|
||||
Message: i18n.LocalizedMessage(c, "message.email_verified"),
|
||||
return c.JSON(http.StatusOK, responses.VerifyEmailResponse{
|
||||
Message: "Email verified successfully",
|
||||
Verified: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ResendVerification handles POST /api/auth/resend-verification/
|
||||
func (h *AuthHandler) ResendVerification(c *gin.Context) {
|
||||
user := middleware.MustGetAuthUser(c)
|
||||
if user == nil {
|
||||
return
|
||||
func (h *AuthHandler) ResendVerification(c echo.Context) error {
|
||||
user, err := middleware.MustGetAuthUser(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code, err := h.authService.ResendVerificationCode(user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrAlreadyVerified) {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.email_already_verified")})
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification")
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{Error: i18n.LocalizedMessage(c, "error.failed_to_resend_verification")})
|
||||
return
|
||||
log.Debug().Err(err).Uint("user_id", user.ID).Msg("Failed to resend verification")
|
||||
return err
|
||||
}
|
||||
|
||||
// Send verification email (async)
|
||||
@@ -271,33 +210,29 @@ func (h *AuthHandler) ResendVerification(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.verification_email_sent")})
|
||||
return c.JSON(http.StatusOK, responses.MessageResponse{Message: "Verification email sent"})
|
||||
}
|
||||
|
||||
// ForgotPassword handles POST /api/auth/forgot-password/
|
||||
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
||||
func (h *AuthHandler) ForgotPassword(c echo.Context) error {
|
||||
var req requests.ForgotPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
code, user, err := h.authService.ForgotPassword(req.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrRateLimitExceeded) {
|
||||
c.JSON(http.StatusTooManyRequests, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.rate_limit_exceeded"),
|
||||
})
|
||||
return
|
||||
var appErr *apperrors.AppError
|
||||
if errors.As(err, &appErr) && appErr.Code == http.StatusTooManyRequests {
|
||||
// Only reveal rate limit errors
|
||||
return err
|
||||
}
|
||||
|
||||
log.Error().Err(err).Str("email", req.Email).Msg("Forgot password failed")
|
||||
// Don't reveal errors to prevent email enumeration
|
||||
// Don't reveal other errors to prevent email enumeration
|
||||
}
|
||||
|
||||
// Send password reset email (async) - only if user found
|
||||
@@ -310,116 +245,82 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Always return success to prevent email enumeration
|
||||
c.JSON(http.StatusOK, responses.ForgotPasswordResponse{
|
||||
Message: i18n.LocalizedMessage(c, "message.password_reset_email_sent"),
|
||||
return c.JSON(http.StatusOK, responses.ForgotPasswordResponse{
|
||||
Message: "Password reset email sent",
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyResetCode handles POST /api/auth/verify-reset-code/
|
||||
func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
|
||||
func (h *AuthHandler) VerifyResetCode(c echo.Context) error {
|
||||
var req requests.VerifyResetCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
message := i18n.LocalizedMessage(c, "error.invalid_verification_code")
|
||||
|
||||
if errors.Is(err, services.ErrCodeExpired) {
|
||||
message = i18n.LocalizedMessage(c, "error.verification_code_expired")
|
||||
} else if errors.Is(err, services.ErrRateLimitExceeded) {
|
||||
status = http.StatusTooManyRequests
|
||||
message = i18n.LocalizedMessage(c, "error.too_many_attempts")
|
||||
}
|
||||
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
log.Debug().Err(err).Str("email", req.Email).Msg("Verify reset code failed")
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
|
||||
Message: i18n.LocalizedMessage(c, "message.reset_code_verified"),
|
||||
return c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
|
||||
Message: "Reset code verified",
|
||||
ResetToken: resetToken,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword handles POST /api/auth/reset-password/
|
||||
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||
func (h *AuthHandler) ResetPassword(c echo.Context) error {
|
||||
var req requests.ResetPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
err := h.authService.ResetPassword(req.ResetToken, req.NewPassword)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
message := i18n.LocalizedMessage(c, "error.invalid_reset_token")
|
||||
|
||||
if errors.Is(err, services.ErrInvalidResetToken) {
|
||||
message = i18n.LocalizedMessage(c, "error.invalid_reset_token")
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
message = i18n.LocalizedMessage(c, "error.password_reset_failed")
|
||||
log.Error().Err(err).Msg("Password reset failed")
|
||||
}
|
||||
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
log.Debug().Err(err).Msg("Password reset failed")
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.ResetPasswordResponse{
|
||||
Message: i18n.LocalizedMessage(c, "message.password_reset_success"),
|
||||
return c.JSON(http.StatusOK, responses.ResetPasswordResponse{
|
||||
Message: "Password reset successful",
|
||||
})
|
||||
}
|
||||
|
||||
// AppleSignIn handles POST /api/auth/apple-sign-in/
|
||||
func (h *AuthHandler) AppleSignIn(c *gin.Context) {
|
||||
func (h *AuthHandler) AppleSignIn(c echo.Context) error {
|
||||
var req requests.AppleSignInRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
if h.appleAuthService == nil {
|
||||
log.Error().Msg("Apple auth service not configured")
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.apple_signin_not_configured"),
|
||||
})
|
||||
return
|
||||
return &apperrors.AppError{
|
||||
Code: 500,
|
||||
MessageKey: "error.apple_signin_not_configured",
|
||||
}
|
||||
}
|
||||
|
||||
response, err := h.authService.AppleSignIn(c.Request.Context(), h.appleAuthService, &req)
|
||||
response, err := h.authService.AppleSignIn(c.Request().Context(), h.appleAuthService, &req)
|
||||
if err != nil {
|
||||
status := http.StatusUnauthorized
|
||||
message := i18n.LocalizedMessage(c, "error.apple_signin_failed")
|
||||
|
||||
if errors.Is(err, services.ErrUserInactive) {
|
||||
message = i18n.LocalizedMessage(c, "error.account_inactive")
|
||||
} else if errors.Is(err, services.ErrAppleSignInFailed) {
|
||||
message = i18n.LocalizedMessage(c, "error.invalid_apple_token")
|
||||
// Check for legacy Apple Sign In error (not yet migrated)
|
||||
if errors.Is(err, services.ErrAppleSignInFailed) {
|
||||
log.Debug().Err(err).Msg("Apple Sign In failed (legacy error)")
|
||||
return apperrors.Unauthorized("error.invalid_apple_token")
|
||||
}
|
||||
|
||||
log.Debug().Err(err).Msg("Apple Sign In failed")
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Send welcome email for new users (async)
|
||||
@@ -431,44 +332,37 @@ func (h *AuthHandler) AppleSignIn(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GoogleSignIn handles POST /api/auth/google-sign-in/
|
||||
func (h *AuthHandler) GoogleSignIn(c *gin.Context) {
|
||||
func (h *AuthHandler) GoogleSignIn(c echo.Context) error {
|
||||
var req requests.GoogleSignInRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.invalid_request_body"),
|
||||
Details: map[string]string{
|
||||
"validation": err.Error(),
|
||||
},
|
||||
})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
if h.googleAuthService == nil {
|
||||
log.Error().Msg("Google auth service not configured")
|
||||
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
|
||||
Error: i18n.LocalizedMessage(c, "error.google_signin_not_configured"),
|
||||
})
|
||||
return
|
||||
return &apperrors.AppError{
|
||||
Code: 500,
|
||||
MessageKey: "error.google_signin_not_configured",
|
||||
}
|
||||
}
|
||||
|
||||
response, err := h.authService.GoogleSignIn(c.Request.Context(), h.googleAuthService, &req)
|
||||
response, err := h.authService.GoogleSignIn(c.Request().Context(), h.googleAuthService, &req)
|
||||
if err != nil {
|
||||
status := http.StatusUnauthorized
|
||||
message := i18n.LocalizedMessage(c, "error.google_signin_failed")
|
||||
|
||||
if errors.Is(err, services.ErrUserInactive) {
|
||||
message = i18n.LocalizedMessage(c, "error.account_inactive")
|
||||
} else if errors.Is(err, services.ErrGoogleSignInFailed) {
|
||||
message = i18n.LocalizedMessage(c, "error.invalid_google_token")
|
||||
// Check for legacy Google Sign In error (not yet migrated)
|
||||
if errors.Is(err, services.ErrGoogleSignInFailed) {
|
||||
log.Debug().Err(err).Msg("Google Sign In failed (legacy error)")
|
||||
return apperrors.Unauthorized("error.invalid_google_token")
|
||||
}
|
||||
|
||||
log.Debug().Err(err).Msg("Google Sign In failed")
|
||||
c.JSON(status, responses.ErrorResponse{Error: message})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Send welcome email for new users (async)
|
||||
@@ -480,5 +374,5 @@ func (h *AuthHandler) GoogleSignIn(c *gin.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/treytartt/casera-api/internal/testutil"
|
||||
)
|
||||
|
||||
func setupAuthHandler(t *testing.T) (*AuthHandler, *gin.Engine, *repositories.UserRepository) {
|
||||
func setupAuthHandler(t *testing.T) (*AuthHandler, *echo.Echo, *repositories.UserRepository) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
cfg := &config.Config{
|
||||
@@ -30,14 +30,14 @@ func setupAuthHandler(t *testing.T) (*AuthHandler, *gin.Engine, *repositories.Us
|
||||
}
|
||||
authService := services.NewAuthService(userRepo, cfg)
|
||||
handler := NewAuthHandler(authService, nil, nil) // No email or cache for tests
|
||||
router := testutil.SetupTestRouter()
|
||||
return handler, router, userRepo
|
||||
e := testutil.SetupTestRouter()
|
||||
return handler, e, userRepo
|
||||
}
|
||||
|
||||
func TestAuthHandler_Register(t *testing.T) {
|
||||
handler, router, _ := setupAuthHandler(t)
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
router.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
|
||||
t.Run("successful registration", func(t *testing.T) {
|
||||
req := requests.RegisterRequest{
|
||||
@@ -48,7 +48,7 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
LastName: "User",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
// Missing email and password
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
Password: "short", // Less than 8 chars
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
@@ -100,13 +100,13 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
Email: "unique1@test.com",
|
||||
Password: "password123",
|
||||
}
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
// Try to register again with same username
|
||||
req.Email = "unique2@test.com"
|
||||
w = testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
w = testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusConflict) // 409 for duplicate resource
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["error"], "Username already taken")
|
||||
@@ -119,13 +119,13 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
Email: "duplicate@test.com",
|
||||
Password: "password123",
|
||||
}
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
// Try to register again with same email
|
||||
req.Username = "user2"
|
||||
w = testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
w = testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusConflict) // 409 for duplicate resource
|
||||
|
||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
||||
assert.Contains(t, response["error"], "Email already registered")
|
||||
@@ -133,10 +133,10 @@ func TestAuthHandler_Register(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login(t *testing.T) {
|
||||
handler, router, _ := setupAuthHandler(t)
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
router.POST("/api/auth/register/", handler.Register)
|
||||
router.POST("/api/auth/login/", handler.Login)
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/login/", handler.Login)
|
||||
|
||||
// Create a test user
|
||||
registerReq := requests.RegisterRequest{
|
||||
@@ -146,7 +146,7 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
}
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", registerReq, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", registerReq, "")
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
t.Run("successful login with username", func(t *testing.T) {
|
||||
@@ -155,7 +155,7 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -177,7 +177,7 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
})
|
||||
@@ -188,7 +188,7 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusUnauthorized)
|
||||
})
|
||||
@@ -213,14 +213,14 @@ func TestAuthHandler_Login(t *testing.T) {
|
||||
// Missing password
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/login/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/login/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthHandler_CurrentUser(t *testing.T) {
|
||||
handler, router, userRepo := setupAuthHandler(t)
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "metest", "me@test.com", "password123")
|
||||
@@ -229,12 +229,12 @@ func TestAuthHandler_CurrentUser(t *testing.T) {
|
||||
userRepo.Update(user)
|
||||
|
||||
// Set up route with mock auth middleware
|
||||
authGroup := router.Group("/api/auth")
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/me/", handler.CurrentUser)
|
||||
|
||||
t.Run("get current user", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/auth/me/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/auth/me/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -248,13 +248,13 @@ func TestAuthHandler_CurrentUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHandler_UpdateProfile(t *testing.T) {
|
||||
handler, router, userRepo := setupAuthHandler(t)
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "updatetest", "update@test.com", "password123")
|
||||
userRepo.Update(user)
|
||||
|
||||
authGroup := router.Group("/api/auth")
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.PUT("/profile/", handler.UpdateProfile)
|
||||
|
||||
@@ -266,7 +266,7 @@ func TestAuthHandler_UpdateProfile(t *testing.T) {
|
||||
LastName: &lastName,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "PUT", "/api/auth/profile/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "PUT", "/api/auth/profile/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -280,10 +280,10 @@ func TestAuthHandler_UpdateProfile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHandler_ForgotPassword(t *testing.T) {
|
||||
handler, router, _ := setupAuthHandler(t)
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
router.POST("/api/auth/register/", handler.Register)
|
||||
router.POST("/api/auth/forgot-password/", handler.ForgotPassword)
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/forgot-password/", handler.ForgotPassword)
|
||||
|
||||
// Create a test user
|
||||
registerReq := requests.RegisterRequest{
|
||||
@@ -291,14 +291,14 @@ func TestAuthHandler_ForgotPassword(t *testing.T) {
|
||||
Email: "forgot@test.com",
|
||||
Password: "password123",
|
||||
}
|
||||
testutil.MakeRequest(router, "POST", "/api/auth/register/", registerReq, "")
|
||||
testutil.MakeRequest(e, "POST", "/api/auth/register/", registerReq, "")
|
||||
|
||||
t.Run("forgot password with valid email", func(t *testing.T) {
|
||||
req := requests.ForgotPasswordRequest{
|
||||
Email: "forgot@test.com",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/forgot-password/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "")
|
||||
|
||||
// Always returns 200 to prevent email enumeration
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
@@ -312,7 +312,7 @@ func TestAuthHandler_ForgotPassword(t *testing.T) {
|
||||
Email: "nonexistent@test.com",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/forgot-password/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/forgot-password/", req, "")
|
||||
|
||||
// Still returns 200 to prevent email enumeration
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
@@ -320,18 +320,18 @@ func TestAuthHandler_ForgotPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHandler_Logout(t *testing.T) {
|
||||
handler, router, userRepo := setupAuthHandler(t)
|
||||
handler, e, userRepo := setupAuthHandler(t)
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
user := testutil.CreateTestUser(t, db, "logouttest", "logout@test.com", "password123")
|
||||
userRepo.Update(user)
|
||||
|
||||
authGroup := router.Group("/api/auth")
|
||||
authGroup := e.Group("/api/auth")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/logout/", handler.Logout)
|
||||
|
||||
t.Run("successful logout", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/logout/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/logout/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -341,10 +341,10 @@ func TestAuthHandler_Logout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthHandler_JSONResponses(t *testing.T) {
|
||||
handler, router, _ := setupAuthHandler(t)
|
||||
handler, e, _ := setupAuthHandler(t)
|
||||
|
||||
router.POST("/api/auth/register/", handler.Register)
|
||||
router.POST("/api/auth/login/", handler.Login)
|
||||
e.POST("/api/auth/register/", handler.Register)
|
||||
e.POST("/api/auth/login/", handler.Login)
|
||||
|
||||
t.Run("register response has correct JSON structure", func(t *testing.T) {
|
||||
req := requests.RegisterRequest{
|
||||
@@ -355,7 +355,7 @@ func TestAuthHandler_JSONResponses(t *testing.T) {
|
||||
LastName: "Test",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -393,7 +393,7 @@ func TestAuthHandler_JSONResponses(t *testing.T) {
|
||||
"username": "test",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/auth/register/", req, "")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/auth/register/", req, "")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -25,190 +24,130 @@ func NewContractorHandler(contractorService *services.ContractorService) *Contra
|
||||
}
|
||||
|
||||
// ListContractors handles GET /api/contractors/
|
||||
func (h *ContractorHandler) ListContractors(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) ListContractors(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
response, err := h.contractorService.ListContractors(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetContractor handles GET /api/contractors/:id/
|
||||
func (h *ContractorHandler) GetContractor(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) GetContractor(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_contractor_id")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.GetContractor(uint(contractorID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrContractorNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateContractor handles POST /api/contractors/
|
||||
func (h *ContractorHandler) CreateContractor(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) CreateContractor(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
var req requests.CreateContractorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.CreateContractor(&req, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateContractor handles PUT/PATCH /api/contractors/:id/
|
||||
func (h *ContractorHandler) UpdateContractor(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) UpdateContractor(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_contractor_id")
|
||||
}
|
||||
|
||||
var req requests.UpdateContractorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.UpdateContractor(uint(contractorID), user.ID, &req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrContractorNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteContractor handles DELETE /api/contractors/:id/
|
||||
func (h *ContractorHandler) DeleteContractor(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) DeleteContractor(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_contractor_id")
|
||||
}
|
||||
|
||||
err = h.contractorService.DeleteContractor(uint(contractorID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrContractorNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.contractor_deleted")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Contractor deleted successfully"})
|
||||
}
|
||||
|
||||
// ToggleFavorite handles POST /api/contractors/:id/toggle-favorite/
|
||||
func (h *ContractorHandler) ToggleFavorite(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) ToggleFavorite(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_contractor_id")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.ToggleFavorite(uint(contractorID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrContractorNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetContractorTasks handles GET /api/contractors/:id/tasks/
|
||||
func (h *ContractorHandler) GetContractorTasks(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) GetContractorTasks(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
contractorID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_contractor_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_contractor_id")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.GetContractorTasks(uint(contractorID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrContractorNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_not_found")})
|
||||
case errors.Is(err, services.ErrContractorAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.contractor_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ListContractorsByResidence handles GET /api/contractors/by-residence/:residence_id/
|
||||
func (h *ContractorHandler) ListContractorsByResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ContractorHandler) ListContractorsByResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
response, err := h.contractorService.ListContractorsByResidence(uint(residenceID), user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetSpecialties handles GET /api/contractors/specialties/
|
||||
func (h *ContractorHandler) GetSpecialties(c *gin.Context) {
|
||||
func (h *ContractorHandler) GetSpecialties(c echo.Context) error {
|
||||
specialties, err := h.contractorService.GetSpecialties()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
c.JSON(http.StatusOK, specialties)
|
||||
return c.JSON(http.StatusOK, specialties)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -33,101 +32,86 @@ func NewDocumentHandler(documentService *services.DocumentService, storageServic
|
||||
}
|
||||
|
||||
// ListDocuments handles GET /api/documents/
|
||||
func (h *DocumentHandler) ListDocuments(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) ListDocuments(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
response, err := h.documentService.ListDocuments(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetDocument handles GET /api/documents/:id/
|
||||
func (h *DocumentHandler) GetDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) GetDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_document_id")
|
||||
}
|
||||
|
||||
response, err := h.documentService.GetDocument(uint(documentID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrDocumentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ListWarranties handles GET /api/documents/warranties/
|
||||
func (h *DocumentHandler) ListWarranties(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) ListWarranties(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
response, err := h.documentService.ListWarranties(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateDocument handles POST /api/documents/
|
||||
// Supports both JSON and multipart form data (for file uploads)
|
||||
func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) CreateDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
var req requests.CreateDocumentRequest
|
||||
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
contentType := c.Request().Header.Get("Content-Type")
|
||||
|
||||
// Check if this is a multipart form request (file upload)
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_parse_form")})
|
||||
return
|
||||
if err := c.Request().ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
return apperrors.BadRequest("error.failed_to_parse_form")
|
||||
}
|
||||
|
||||
// Parse residence_id (required)
|
||||
residenceIDStr := c.PostForm("residence_id")
|
||||
residenceIDStr := c.FormValue("residence_id")
|
||||
if residenceIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_id_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.residence_id_required")
|
||||
}
|
||||
residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
req.ResidenceID = uint(residenceID)
|
||||
|
||||
// Parse title (required)
|
||||
req.Title = c.PostForm("title")
|
||||
req.Title = c.FormValue("title")
|
||||
if req.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.title_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.title_required")
|
||||
}
|
||||
|
||||
// Parse optional fields
|
||||
req.Description = c.PostForm("description")
|
||||
req.Vendor = c.PostForm("vendor")
|
||||
req.SerialNumber = c.PostForm("serial_number")
|
||||
req.ModelNumber = c.PostForm("model_number")
|
||||
req.Description = c.FormValue("description")
|
||||
req.Vendor = c.FormValue("vendor")
|
||||
req.SerialNumber = c.FormValue("serial_number")
|
||||
req.ModelNumber = c.FormValue("model_number")
|
||||
|
||||
// Parse document_type
|
||||
if docType := c.PostForm("document_type"); docType != "" {
|
||||
if docType := c.FormValue("document_type"); docType != "" {
|
||||
dt := models.DocumentType(docType)
|
||||
req.DocumentType = dt
|
||||
}
|
||||
|
||||
// Parse task_id (optional)
|
||||
if taskIDStr := c.PostForm("task_id"); taskIDStr != "" {
|
||||
if taskIDStr := c.FormValue("task_id"); taskIDStr != "" {
|
||||
if taskID, err := strconv.ParseUint(taskIDStr, 10, 32); err == nil {
|
||||
tid := uint(taskID)
|
||||
req.TaskID = &tid
|
||||
@@ -135,14 +119,14 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Parse purchase_price (optional)
|
||||
if priceStr := c.PostForm("purchase_price"); priceStr != "" {
|
||||
if priceStr := c.FormValue("purchase_price"); priceStr != "" {
|
||||
if price, err := decimal.NewFromString(priceStr); err == nil {
|
||||
req.PurchasePrice = &price
|
||||
}
|
||||
}
|
||||
|
||||
// Parse purchase_date (optional)
|
||||
if dateStr := c.PostForm("purchase_date"); dateStr != "" {
|
||||
if dateStr := c.FormValue("purchase_date"); dateStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, dateStr); err == nil {
|
||||
req.PurchaseDate = &t
|
||||
} else if t, err := time.Parse("2006-01-02", dateStr); err == nil {
|
||||
@@ -151,7 +135,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Parse expiry_date (optional)
|
||||
if dateStr := c.PostForm("expiry_date"); dateStr != "" {
|
||||
if dateStr := c.FormValue("expiry_date"); dateStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, dateStr); err == nil {
|
||||
req.ExpiryDate = &t
|
||||
} else if t, err := time.Parse("2006-01-02", dateStr); err == nil {
|
||||
@@ -171,8 +155,7 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
if uploadedFile != nil && h.storageService != nil {
|
||||
result, err := h.storageService.Upload(uploadedFile, "documents")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_upload_file")})
|
||||
return
|
||||
return apperrors.BadRequest("error.failed_to_upload_file")
|
||||
}
|
||||
req.FileURL = result.URL
|
||||
req.FileName = result.FileName
|
||||
@@ -182,122 +165,79 @@ func (h *DocumentHandler) CreateDocument(c *gin.Context) {
|
||||
}
|
||||
} else {
|
||||
// Standard JSON request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
response, err := h.documentService.CreateDocument(&req, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateDocument handles PUT/PATCH /api/documents/:id/
|
||||
func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) UpdateDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_document_id")
|
||||
}
|
||||
|
||||
var req requests.UpdateDocumentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.documentService.UpdateDocument(uint(documentID), user.ID, &req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrDocumentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteDocument handles DELETE /api/documents/:id/
|
||||
func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) DeleteDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_document_id")
|
||||
}
|
||||
|
||||
err = h.documentService.DeleteDocument(uint(documentID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrDocumentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deleted")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document deleted successfully"})
|
||||
}
|
||||
|
||||
// ActivateDocument handles POST /api/documents/:id/activate/
|
||||
func (h *DocumentHandler) ActivateDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) ActivateDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_document_id")
|
||||
}
|
||||
|
||||
response, err := h.documentService.ActivateDocument(uint(documentID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrDocumentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_activated"), "document": response})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document activated successfully", "document": response})
|
||||
}
|
||||
|
||||
// DeactivateDocument handles POST /api/documents/:id/deactivate/
|
||||
func (h *DocumentHandler) DeactivateDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *DocumentHandler) DeactivateDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
documentID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_document_id")
|
||||
}
|
||||
|
||||
response, err := h.documentService.DeactivateDocument(uint(documentID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrDocumentNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")})
|
||||
case errors.Is(err, services.ErrDocumentAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deactivated"), "document": response})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Document deactivated successfully", "document": response})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/repositories"
|
||||
@@ -39,132 +39,117 @@ func NewMediaHandler(
|
||||
|
||||
// ServeDocument serves a document file with access control
|
||||
// GET /api/media/document/:id
|
||||
func (h *MediaHandler) ServeDocument(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *MediaHandler) ServeDocument(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_id")
|
||||
}
|
||||
|
||||
// Get document
|
||||
doc, err := h.documentRepo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
|
||||
// Check access to residence
|
||||
hasAccess, err := h.residenceRepo.HasAccess(doc.ResidenceID, user.ID)
|
||||
if err != nil || !hasAccess {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
return apperrors.Forbidden("error.access_denied")
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
filePath := h.resolveFilePath(doc.FileURL)
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.file_not_found")
|
||||
}
|
||||
|
||||
// Set caching headers (private, 1 hour)
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
c.File(filePath)
|
||||
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
|
||||
return c.File(filePath)
|
||||
}
|
||||
|
||||
// ServeDocumentImage serves a document image with access control
|
||||
// GET /api/media/document-image/:id
|
||||
func (h *MediaHandler) ServeDocumentImage(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *MediaHandler) ServeDocumentImage(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_id")
|
||||
}
|
||||
|
||||
// Get document image
|
||||
img, err := h.documentRepo.FindImageByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Image not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.image_not_found")
|
||||
}
|
||||
|
||||
// Get parent document to check residence access
|
||||
doc, err := h.documentRepo.FindByID(img.DocumentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Parent document not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
|
||||
// Check access to residence
|
||||
hasAccess, err := h.residenceRepo.HasAccess(doc.ResidenceID, user.ID)
|
||||
if err != nil || !hasAccess {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
return apperrors.Forbidden("error.access_denied")
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
filePath := h.resolveFilePath(img.ImageURL)
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.file_not_found")
|
||||
}
|
||||
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
c.File(filePath)
|
||||
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
|
||||
return c.File(filePath)
|
||||
}
|
||||
|
||||
// ServeCompletionImage serves a task completion image with access control
|
||||
// GET /api/media/completion-image/:id
|
||||
func (h *MediaHandler) ServeCompletionImage(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *MediaHandler) ServeCompletionImage(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_id")
|
||||
}
|
||||
|
||||
// Get completion image
|
||||
img, err := h.taskRepo.FindCompletionImageByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Image not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.image_not_found")
|
||||
}
|
||||
|
||||
// Get the completion to get the task
|
||||
completion, err := h.taskRepo.FindCompletionByID(img.CompletionID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Completion not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.completion_not_found")
|
||||
}
|
||||
|
||||
// Get task to check residence access
|
||||
task, err := h.taskRepo.FindByID(completion.TaskID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.task_not_found")
|
||||
}
|
||||
|
||||
// Check access to residence
|
||||
hasAccess, err := h.residenceRepo.HasAccess(task.ResidenceID, user.ID)
|
||||
if err != nil || !hasAccess {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
return apperrors.Forbidden("error.access_denied")
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
filePath := h.resolveFilePath(img.ImageURL)
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
return
|
||||
return apperrors.NotFound("error.file_not_found")
|
||||
}
|
||||
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
c.File(filePath)
|
||||
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
|
||||
return c.File(filePath)
|
||||
}
|
||||
|
||||
// resolveFilePath converts a stored URL to an actual file path
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -24,17 +23,17 @@ func NewNotificationHandler(notificationService *services.NotificationService) *
|
||||
}
|
||||
|
||||
// ListNotifications handles GET /api/notifications/
|
||||
func (h *NotificationHandler) ListNotifications(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) ListNotifications(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
limit := 50
|
||||
offset := 0
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if l := c.QueryParam("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
if o := c.Query("offset"); o != "" {
|
||||
if o := c.QueryParam("offset"); o != "" {
|
||||
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
}
|
||||
@@ -42,157 +41,132 @@ func (h *NotificationHandler) ListNotifications(c *gin.Context) {
|
||||
|
||||
notifications, err := h.notificationService.GetNotifications(user.ID, limit, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"count": len(notifications),
|
||||
"results": notifications,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUnreadCount handles GET /api/notifications/unread-count/
|
||||
func (h *NotificationHandler) GetUnreadCount(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) GetUnreadCount(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
count, err := h.notificationService.GetUnreadCount(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"unread_count": count})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"unread_count": count})
|
||||
}
|
||||
|
||||
// MarkAsRead handles POST /api/notifications/:id/read/
|
||||
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) MarkAsRead(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
notificationID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_notification_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_notification_id")
|
||||
}
|
||||
|
||||
err = h.notificationService.MarkAsRead(uint(notificationID), user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrNotificationNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.notification_not_found")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.notification_marked_read")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "message.notification_marked_read"})
|
||||
}
|
||||
|
||||
// MarkAllAsRead handles POST /api/notifications/mark-all-read/
|
||||
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) MarkAllAsRead(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
err := h.notificationService.MarkAllAsRead(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.all_notifications_marked_read")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "message.all_notifications_marked_read"})
|
||||
}
|
||||
|
||||
// GetPreferences handles GET /api/notifications/preferences/
|
||||
func (h *NotificationHandler) GetPreferences(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) GetPreferences(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
prefs, err := h.notificationService.GetPreferences(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, prefs)
|
||||
return c.JSON(http.StatusOK, prefs)
|
||||
}
|
||||
|
||||
// UpdatePreferences handles PUT/PATCH /api/notifications/preferences/
|
||||
func (h *NotificationHandler) UpdatePreferences(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) UpdatePreferences(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.UpdatePreferencesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
prefs, err := h.notificationService.UpdatePreferences(user.ID, &req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, prefs)
|
||||
return c.JSON(http.StatusOK, prefs)
|
||||
}
|
||||
|
||||
// RegisterDevice handles POST /api/notifications/devices/
|
||||
func (h *NotificationHandler) RegisterDevice(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) RegisterDevice(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.RegisterDeviceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
device, err := h.notificationService.RegisterDevice(user.ID, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrInvalidPlatform) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_platform")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, device)
|
||||
return c.JSON(http.StatusCreated, device)
|
||||
}
|
||||
|
||||
// ListDevices handles GET /api/notifications/devices/
|
||||
func (h *NotificationHandler) ListDevices(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) ListDevices(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
devices, err := h.notificationService.ListDevices(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, devices)
|
||||
return c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
|
||||
// DeleteDevice handles DELETE /api/notifications/devices/:id/
|
||||
func (h *NotificationHandler) DeleteDevice(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *NotificationHandler) DeleteDevice(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
deviceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_device_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_device_id")
|
||||
}
|
||||
|
||||
platform := c.Query("platform")
|
||||
platform := c.QueryParam("platform")
|
||||
if platform == "" {
|
||||
platform = "ios" // Default to iOS
|
||||
}
|
||||
|
||||
err = h.notificationService.DeleteDevice(uint(deviceID), platform, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrInvalidPlatform) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_platform")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.device_removed")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "message.device_removed"})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
"github.com/treytartt/casera-api/internal/validator"
|
||||
)
|
||||
|
||||
// ResidenceHandler handles residence-related HTTP requests
|
||||
@@ -31,343 +32,252 @@ func NewResidenceHandler(residenceService *services.ResidenceService, pdfService
|
||||
}
|
||||
|
||||
// ListResidences handles GET /api/residences/
|
||||
func (h *ResidenceHandler) ListResidences(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) ListResidences(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
response, err := h.residenceService.ListResidences(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetMyResidences handles GET /api/residences/my-residences/
|
||||
func (h *ResidenceHandler) GetMyResidences(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GetMyResidences(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
response, err := h.residenceService.GetMyResidences(user.ID, userNow)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetSummary handles GET /api/residences/summary/
|
||||
// Returns just the task statistics summary without full residence data
|
||||
func (h *ResidenceHandler) GetSummary(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GetSummary(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
summary, err := h.residenceService.GetSummary(user.ID, userNow)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, summary)
|
||||
return c.JSON(http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// GetResidence handles GET /api/residences/:id/
|
||||
func (h *ResidenceHandler) GetResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GetResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
response, err := h.residenceService.GetResidence(uint(residenceID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateResidence handles POST /api/residences/
|
||||
func (h *ResidenceHandler) CreateResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) CreateResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req requests.CreateResidenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
response, err := h.residenceService.CreateResidence(&req, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrPropertiesLimitReached) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.properties_limit_reached")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateResidence handles PUT/PATCH /api/residences/:id/
|
||||
func (h *ResidenceHandler) UpdateResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) UpdateResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
var req requests.UpdateResidenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, validator.FormatValidationErrors(err))
|
||||
}
|
||||
|
||||
response, err := h.residenceService.UpdateResidence(uint(residenceID), user.ID, &req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteResidence handles DELETE /api/residences/:id/
|
||||
func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) DeleteResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
response, err := h.residenceService.DeleteResidence(uint(residenceID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
|
||||
func (h *ResidenceHandler) GenerateShareCode(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GenerateShareCode(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
var req requests.GenerateShareCodeRequest
|
||||
// Request body is optional
|
||||
c.ShouldBindJSON(&req)
|
||||
c.Bind(&req)
|
||||
|
||||
response, err := h.residenceService.GenerateShareCode(uint(residenceID), user.ID, req.ExpiresInHours)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GenerateSharePackage handles POST /api/residences/:id/generate-share-package/
|
||||
// Returns a share code with metadata for creating a .casera package file
|
||||
func (h *ResidenceHandler) GenerateSharePackage(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GenerateSharePackage(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
var req requests.GenerateShareCodeRequest
|
||||
// Request body is optional (for expires_in_hours)
|
||||
c.ShouldBindJSON(&req)
|
||||
c.Bind(&req)
|
||||
|
||||
response, err := h.residenceService.GenerateSharePackage(uint(residenceID), user.ID, req.ExpiresInHours)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// JoinWithCode handles POST /api/residences/join-with-code/
|
||||
func (h *ResidenceHandler) JoinWithCode(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) JoinWithCode(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req requests.JoinWithCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.residenceService.JoinWithCode(req.Code, user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrShareCodeInvalid):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.share_code_invalid")})
|
||||
case errors.Is(err, services.ErrShareCodeExpired):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.share_code_expired")})
|
||||
case errors.Is(err, services.ErrUserAlreadyMember):
|
||||
c.JSON(http.StatusConflict, gin.H{"error": i18n.LocalizedMessage(c, "error.user_already_member")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetResidenceUsers handles GET /api/residences/:id/users/
|
||||
func (h *ResidenceHandler) GetResidenceUsers(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GetResidenceUsers(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
users, err := h.residenceService.GetResidenceUsers(uint(residenceID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, users)
|
||||
return c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
// RemoveResidenceUser handles DELETE /api/residences/:id/users/:user_id/
|
||||
func (h *ResidenceHandler) RemoveResidenceUser(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) RemoveResidenceUser(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
userIDToRemove, err := strconv.ParseUint(c.Param("user_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_user_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_user_id")
|
||||
}
|
||||
|
||||
err = h.residenceService.RemoveUser(uint(residenceID), uint(userIDToRemove), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrNotResidenceOwner):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.not_residence_owner")})
|
||||
case errors.Is(err, services.ErrCannotRemoveOwner):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.cannot_remove_owner")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.user_removed")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": i18n.LocalizedMessage(c, "message.user_removed")})
|
||||
}
|
||||
|
||||
// GetResidenceTypes handles GET /api/residences/types/
|
||||
func (h *ResidenceHandler) GetResidenceTypes(c *gin.Context) {
|
||||
func (h *ResidenceHandler) GetResidenceTypes(c echo.Context) error {
|
||||
types, err := h.residenceService.GetResidenceTypes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, types)
|
||||
return c.JSON(http.StatusOK, types)
|
||||
}
|
||||
|
||||
// GenerateTasksReport handles POST /api/residences/:id/generate-tasks-report/
|
||||
// Generates a PDF report of tasks for the residence and emails it
|
||||
func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *ResidenceHandler) GenerateTasksReport(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
// Optional request body for email recipient
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
c.Bind(&req)
|
||||
|
||||
// Generate the report data
|
||||
report, err := h.residenceService.GenerateTasksReport(uint(residenceID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_not_found")})
|
||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine recipient email
|
||||
@@ -415,7 +325,7 @@ func (h *ResidenceHandler) GenerateTasksReport(c *gin.Context) {
|
||||
message = i18n.LocalizedMessage(c, "message.tasks_report_email_failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": message,
|
||||
"residence_name": report.ResidenceName,
|
||||
"recipient_email": recipientEmail,
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -19,22 +19,22 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupResidenceHandler(t *testing.T) (*ResidenceHandler, *gin.Engine, *gorm.DB) {
|
||||
func setupResidenceHandler(t *testing.T) (*ResidenceHandler, *echo.Echo, *gorm.DB) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
cfg := &config.Config{}
|
||||
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
|
||||
handler := NewResidenceHandler(residenceService, nil, nil)
|
||||
router := testutil.SetupTestRouter()
|
||||
return handler, router, db
|
||||
e := testutil.SetupTestRouter()
|
||||
return handler, e, db
|
||||
}
|
||||
|
||||
func TestResidenceHandler_CreateResidence(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/", handler.CreateResidence)
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestResidenceHandler_CreateResidence(t *testing.T) {
|
||||
PostalCode: "78701",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestResidenceHandler_CreateResidence(t *testing.T) {
|
||||
IsPrimary: &isPrimary,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -114,28 +114,28 @@ func TestResidenceHandler_CreateResidence(t *testing.T) {
|
||||
// Missing name - this is required
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResidenceHandler_GetResidence(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/:id/", handler.GetResidence)
|
||||
|
||||
otherAuthGroup := router.Group("/api/other-residences")
|
||||
otherAuthGroup := e.Group("/api/other-residences")
|
||||
otherAuthGroup.Use(testutil.MockAuthMiddleware(otherUser))
|
||||
otherAuthGroup.GET("/:id/", handler.GetResidence)
|
||||
|
||||
t.Run("get own residence", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -148,37 +148,37 @@ func TestResidenceHandler_GetResidence(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("get residence with invalid ID", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/residences/invalid/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/residences/invalid/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("get non-existent residence", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/residences/9999/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/residences/9999/", nil, "test-token")
|
||||
|
||||
// Returns 403 (access denied) rather than 404 to not reveal whether an ID exists
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("access denied for other user", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/other-residences/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-residences/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResidenceHandler_ListResidences(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
testutil.CreateTestResidence(t, db, user.ID, "House 1")
|
||||
testutil.CreateTestResidence(t, db, user.ID, "House 2")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/", handler.ListResidences)
|
||||
|
||||
t.Run("list residences", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/residences/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/residences/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -191,7 +191,7 @@ func TestResidenceHandler_ListResidences(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResidenceHandler_UpdateResidence(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Original Name")
|
||||
@@ -200,11 +200,11 @@ func TestResidenceHandler_UpdateResidence(t *testing.T) {
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
residenceRepo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.PUT("/:id/", handler.UpdateResidence)
|
||||
|
||||
sharedGroup := router.Group("/api/shared-residences")
|
||||
sharedGroup := e.Group("/api/shared-residences")
|
||||
sharedGroup.Use(testutil.MockAuthMiddleware(sharedUser))
|
||||
sharedGroup.PUT("/:id/", handler.UpdateResidence)
|
||||
|
||||
@@ -216,7 +216,7 @@ func TestResidenceHandler_UpdateResidence(t *testing.T) {
|
||||
City: &newCity,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "PUT", fmt.Sprintf("/api/residences/%d/", residence.ID), req, "test-token")
|
||||
w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/residences/%d/", residence.ID), req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -239,14 +239,14 @@ func TestResidenceHandler_UpdateResidence(t *testing.T) {
|
||||
Name: &newName,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "PUT", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), req, "test-token")
|
||||
w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResidenceHandler_DeleteResidence(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "To Delete")
|
||||
@@ -254,22 +254,22 @@ func TestResidenceHandler_DeleteResidence(t *testing.T) {
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
residenceRepo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.DELETE("/:id/", handler.DeleteResidence)
|
||||
|
||||
sharedGroup := router.Group("/api/shared-residences")
|
||||
sharedGroup := e.Group("/api/shared-residences")
|
||||
sharedGroup.Use(testutil.MockAuthMiddleware(sharedUser))
|
||||
sharedGroup.DELETE("/:id/", handler.DeleteResidence)
|
||||
|
||||
t.Run("shared user cannot delete", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/shared-residences/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("owner can delete", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -285,11 +285,11 @@ func TestResidenceHandler_DeleteResidence(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResidenceHandler_GenerateShareCode(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Share Test")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/generate-share-code/", handler.GenerateShareCode)
|
||||
|
||||
@@ -298,7 +298,7 @@ func TestResidenceHandler_GenerateShareCode(t *testing.T) {
|
||||
ExpiresInHours: 24,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/residences/%d/generate-share-code/", residence.ID), req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/residences/%d/generate-share-code/", residence.ID), req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -314,7 +314,7 @@ func TestResidenceHandler_GenerateShareCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
newUser := testutil.CreateTestUser(t, db, "newuser", "new@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Join Test")
|
||||
@@ -326,11 +326,11 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
||||
residenceService := services.NewResidenceService(residenceRepo, userRepo, cfg)
|
||||
shareResp, _ := residenceService.GenerateShareCode(residence.ID, owner.ID, 24)
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(newUser))
|
||||
authGroup.POST("/join-with-code/", handler.JoinWithCode)
|
||||
|
||||
ownerGroup := router.Group("/api/owner-residences")
|
||||
ownerGroup := e.Group("/api/owner-residences")
|
||||
ownerGroup.Use(testutil.MockAuthMiddleware(owner))
|
||||
ownerGroup.POST("/join-with-code/", handler.JoinWithCode)
|
||||
|
||||
@@ -339,7 +339,7 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
||||
Code: shareResp.ShareCode.Code,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/join-with-code/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -363,7 +363,7 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
||||
Code: shareResp2.ShareCode.Code,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/owner-residences/join-with-code/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/owner-residences/join-with-code/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusConflict)
|
||||
})
|
||||
@@ -373,14 +373,14 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
||||
Code: "ABCDEF", // Valid length (6) but non-existent code
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/join-with-code/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/join-with-code/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResidenceHandler_GetResidenceUsers(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Users Test")
|
||||
@@ -388,12 +388,12 @@ func TestResidenceHandler_GetResidenceUsers(t *testing.T) {
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
residenceRepo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(owner))
|
||||
authGroup.GET("/:id/users/", handler.GetResidenceUsers)
|
||||
|
||||
t.Run("get residence users", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/residences/%d/users/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/residences/%d/users/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -406,7 +406,7 @@ func TestResidenceHandler_GetResidenceUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResidenceHandler_RemoveUser(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
owner := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
sharedUser := testutil.CreateTestUser(t, db, "shared", "shared@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, owner.ID, "Remove Test")
|
||||
@@ -414,12 +414,12 @@ func TestResidenceHandler_RemoveUser(t *testing.T) {
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
residenceRepo.AddUser(residence.ID, sharedUser.ID)
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(owner))
|
||||
authGroup.DELETE("/:id/users/:user_id/", handler.RemoveResidenceUser)
|
||||
|
||||
t.Run("remove shared user", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, sharedUser.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, sharedUser.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -428,23 +428,23 @@ func TestResidenceHandler_RemoveUser(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("cannot remove owner", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, owner.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/residences/%d/users/%d/", residence.ID, owner.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResidenceHandler_GetResidenceTypes(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/types/", handler.GetResidenceTypes)
|
||||
|
||||
t.Run("get residence types", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/residences/types/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/residences/types/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -457,10 +457,10 @@ func TestResidenceHandler_GetResidenceTypes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResidenceHandler_JSONResponses(t *testing.T) {
|
||||
handler, router, db := setupResidenceHandler(t)
|
||||
handler, e, db := setupResidenceHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
|
||||
authGroup := router.Group("/api/residences")
|
||||
authGroup := e.Group("/api/residences")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/", handler.CreateResidence)
|
||||
authGroup.GET("/", handler.ListResidences)
|
||||
@@ -474,7 +474,7 @@ func TestResidenceHandler_JSONResponses(t *testing.T) {
|
||||
PostalCode: "78701",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/residences/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/residences/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -513,7 +513,7 @@ func TestResidenceHandler_JSONResponses(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("list response returns array", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/residences/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/residences/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
@@ -51,20 +51,19 @@ func NewStaticDataHandler(
|
||||
|
||||
// GetStaticData handles GET /api/static_data/
|
||||
// Returns all lookup/reference data in a single response with ETag support
|
||||
func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
func (h *StaticDataHandler) GetStaticData(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// Check If-None-Match header for conditional request
|
||||
// Strip W/ prefix if present (added by reverse proxy, but we store without it)
|
||||
clientETag := strings.TrimPrefix(c.GetHeader("If-None-Match"), "W/")
|
||||
clientETag := strings.TrimPrefix(c.Request().Header.Get("If-None-Match"), "W/")
|
||||
|
||||
// Try to get cached ETag first (fast path for 304 responses)
|
||||
if h.cache != nil && clientETag != "" {
|
||||
cachedETag, err := h.cache.GetSeededDataETag(ctx)
|
||||
if err == nil && cachedETag == clientETag {
|
||||
// Client has the latest data, return 304 Not Modified
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
return c.NoContent(http.StatusNotModified)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +75,10 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
// Cache hit - get the ETag and return data
|
||||
etag, etagErr := h.cache.GetSeededDataETag(ctx)
|
||||
if etagErr == nil {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
c.Response().Header().Set("ETag", etag)
|
||||
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
|
||||
}
|
||||
c.JSON(http.StatusOK, cachedData)
|
||||
return
|
||||
return c.JSON(http.StatusOK, cachedData)
|
||||
} else if err != redis.Nil {
|
||||
// Log cache error but continue to fetch from DB
|
||||
log.Warn().Err(err).Msg("Failed to get cached seeded data")
|
||||
@@ -90,38 +88,32 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
// Cache miss - fetch all data from services
|
||||
residenceTypes, err := h.residenceService.GetResidenceTypes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_residence_types")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
taskCategories, err := h.taskService.GetCategories()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_categories")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
taskPriorities, err := h.taskService.GetPriorities()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_priorities")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
taskFrequencies, err := h.taskService.GetFrequencies()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_frequencies")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
contractorSpecialties, err := h.contractorService.GetSpecialties()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_contractor_specialties")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
taskTemplates, err := h.taskTemplateService.GetGrouped()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_templates")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Build response
|
||||
@@ -140,19 +132,19 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
|
||||
if cacheErr != nil {
|
||||
log.Warn().Err(cacheErr).Msg("Failed to cache seeded data")
|
||||
} else {
|
||||
c.Header("ETag", etag)
|
||||
c.Header("Cache-Control", "private, max-age=3600")
|
||||
c.Response().Header().Set("ETag", etag)
|
||||
c.Response().Header().Set("Cache-Control", "private, max-age=3600")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, seededData)
|
||||
return c.JSON(http.StatusOK, seededData)
|
||||
}
|
||||
|
||||
// RefreshStaticData handles POST /api/static_data/refresh/
|
||||
// This is a no-op since data is fetched fresh each time
|
||||
// Kept for API compatibility with mobile clients
|
||||
func (h *StaticDataHandler) RefreshStaticData(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
func (h *StaticDataHandler) RefreshStaticData(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": i18n.LocalizedMessage(c, "message.static_data_refreshed"),
|
||||
"status": "success",
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -23,91 +22,80 @@ func NewSubscriptionHandler(subscriptionService *services.SubscriptionService) *
|
||||
}
|
||||
|
||||
// GetSubscription handles GET /api/subscription/
|
||||
func (h *SubscriptionHandler) GetSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
subscription, err := h.subscriptionService.GetSubscription(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, subscription)
|
||||
return c.JSON(http.StatusOK, subscription)
|
||||
}
|
||||
|
||||
// GetSubscriptionStatus handles GET /api/subscription/status/
|
||||
func (h *SubscriptionHandler) GetSubscriptionStatus(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetSubscriptionStatus(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
status, err := h.subscriptionService.GetSubscriptionStatus(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
return c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// GetUpgradeTrigger handles GET /api/subscription/upgrade-trigger/:key/
|
||||
func (h *SubscriptionHandler) GetUpgradeTrigger(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetUpgradeTrigger(c echo.Context) error {
|
||||
key := c.Param("key")
|
||||
|
||||
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrUpgradeTriggerNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.upgrade_trigger_not_found")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, trigger)
|
||||
return c.JSON(http.StatusOK, trigger)
|
||||
}
|
||||
|
||||
// GetAllUpgradeTriggers handles GET /api/subscription/upgrade-triggers/
|
||||
func (h *SubscriptionHandler) GetAllUpgradeTriggers(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetAllUpgradeTriggers(c echo.Context) error {
|
||||
triggers, err := h.subscriptionService.GetAllUpgradeTriggers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, triggers)
|
||||
return c.JSON(http.StatusOK, triggers)
|
||||
}
|
||||
|
||||
// GetFeatureBenefits handles GET /api/subscription/features/
|
||||
func (h *SubscriptionHandler) GetFeatureBenefits(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetFeatureBenefits(c echo.Context) error {
|
||||
benefits, err := h.subscriptionService.GetFeatureBenefits()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, benefits)
|
||||
return c.JSON(http.StatusOK, benefits)
|
||||
}
|
||||
|
||||
// GetPromotions handles GET /api/subscription/promotions/
|
||||
func (h *SubscriptionHandler) GetPromotions(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetPromotions(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
promotions, err := h.subscriptionService.GetActivePromotions(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, promotions)
|
||||
return c.JSON(http.StatusOK, promotions)
|
||||
}
|
||||
|
||||
// ProcessPurchase handles POST /api/subscription/purchase/
|
||||
func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) ProcessPurchase(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.ProcessPurchaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
var subscription *services.SubscriptionResponse
|
||||
@@ -117,53 +105,48 @@ func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
||||
case "ios":
|
||||
// StoreKit 2 uses transaction_id, StoreKit 1 uses receipt_data
|
||||
if req.TransactionID == "" && req.ReceiptData == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.receipt_data_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.receipt_data_required")
|
||||
}
|
||||
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData, req.TransactionID)
|
||||
case "android":
|
||||
if req.PurchaseToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.purchase_token_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.purchase_token_required")
|
||||
}
|
||||
subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken, req.ProductID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_upgraded"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_upgraded",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
// CancelSubscription handles POST /api/subscription/cancel/
|
||||
func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) CancelSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
subscription, err := h.subscriptionService.CancelSubscription(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_cancelled"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_cancelled",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreSubscription handles POST /api/subscription/restore/
|
||||
func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) RestoreSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.ProcessPurchaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
// Same logic as ProcessPurchase - validates receipt/token and restores
|
||||
@@ -178,12 +161,11 @@ func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_restored"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_restored",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/config"
|
||||
@@ -93,27 +93,24 @@ type AppleRenewalInfo struct {
|
||||
}
|
||||
|
||||
// HandleAppleWebhook handles POST /api/subscription/webhook/apple/
|
||||
func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c echo.Context) error {
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to read body: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "failed to read request body"})
|
||||
}
|
||||
|
||||
var payload AppleNotificationPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
log.Printf("Apple Webhook: Failed to parse payload: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
// Decode and verify the signed payload (JWS)
|
||||
notification, err := h.decodeAppleSignedPayload(payload.SignedPayload)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to decode signed payload: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signed payload"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid signed payload"})
|
||||
}
|
||||
|
||||
log.Printf("Apple Webhook: Received %s (subtype: %s) for bundle %s",
|
||||
@@ -125,8 +122,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
if notification.Data.BundleID != cfg.AppleIAP.BundleID {
|
||||
log.Printf("Apple Webhook: Bundle ID mismatch: got %s, expected %s",
|
||||
notification.Data.BundleID, cfg.AppleIAP.BundleID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bundle ID mismatch"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "bundle ID mismatch"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +130,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
transactionInfo, err := h.decodeAppleTransaction(notification.Data.SignedTransactionInfo)
|
||||
if err != nil {
|
||||
log.Printf("Apple Webhook: Failed to decode transaction: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid transaction info"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid transaction info"})
|
||||
}
|
||||
|
||||
// Decode renewal info if present
|
||||
@@ -151,7 +146,7 @@ func (h *SubscriptionWebhookHandler) HandleAppleWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Always return 200 OK to acknowledge receipt
|
||||
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "received"})
|
||||
}
|
||||
|
||||
// decodeAppleSignedPayload decodes and verifies an Apple JWS payload
|
||||
@@ -454,41 +449,36 @@ const (
|
||||
)
|
||||
|
||||
// HandleGoogleWebhook handles POST /api/subscription/webhook/google/
|
||||
func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c echo.Context) error {
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
log.Printf("Google Webhook: Failed to read body: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request body"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "failed to read request body"})
|
||||
}
|
||||
|
||||
var notification GoogleNotification
|
||||
if err := json.Unmarshal(body, ¬ification); err != nil {
|
||||
log.Printf("Google Webhook: Failed to parse notification: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid notification"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid notification"})
|
||||
}
|
||||
|
||||
// Decode the base64 data
|
||||
data, err := base64.StdEncoding.DecodeString(notification.Message.Data)
|
||||
if err != nil {
|
||||
log.Printf("Google Webhook: Failed to decode message data: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message data"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid message data"})
|
||||
}
|
||||
|
||||
var devNotification GoogleDeveloperNotification
|
||||
if err := json.Unmarshal(data, &devNotification); err != nil {
|
||||
log.Printf("Google Webhook: Failed to parse developer notification: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid developer notification"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "invalid developer notification"})
|
||||
}
|
||||
|
||||
// Handle test notification
|
||||
if devNotification.TestNotification != nil {
|
||||
log.Printf("Google Webhook: Received test notification")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "test received"})
|
||||
return
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "test received"})
|
||||
}
|
||||
|
||||
// Verify package name
|
||||
@@ -497,8 +487,7 @@ func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
if devNotification.PackageName != cfg.GoogleIAP.PackageName {
|
||||
log.Printf("Google Webhook: Package name mismatch: got %s, expected %s",
|
||||
devNotification.PackageName, cfg.GoogleIAP.PackageName)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "package name mismatch"})
|
||||
return
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "package name mismatch"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +500,7 @@ func (h *SubscriptionWebhookHandler) HandleGoogleWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Acknowledge the message
|
||||
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"status": "received"})
|
||||
}
|
||||
|
||||
// processGoogleSubscriptionNotification handles Google subscription events
|
||||
@@ -736,7 +725,7 @@ func (h *SubscriptionWebhookHandler) VerifyAppleSignature(signedPayload string)
|
||||
}
|
||||
|
||||
// VerifyGooglePubSubToken verifies the Pub/Sub push token (if configured)
|
||||
func (h *SubscriptionWebhookHandler) VerifyGooglePubSubToken(c *gin.Context) bool {
|
||||
func (h *SubscriptionWebhookHandler) VerifyGooglePubSubToken(c echo.Context) bool {
|
||||
// If you configured a push endpoint with authentication, verify here
|
||||
// The token is typically in the Authorization header
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/dto/requests"
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -33,55 +32,44 @@ func NewTaskHandler(taskService *services.TaskService, storageService *services.
|
||||
}
|
||||
|
||||
// ListTasks handles GET /api/tasks/
|
||||
func (h *TaskHandler) ListTasks(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) ListTasks(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
response, err := h.taskService.ListTasks(user.ID, userNow)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetTask handles GET /api/tasks/:id/
|
||||
func (h *TaskHandler) GetTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) GetTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.GetTask(uint(taskID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetTasksByResidence handles GET /api/tasks/by-residence/:residence_id/
|
||||
func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) GetTasksByResidence(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
residenceID, err := strconv.ParseUint(c.Param("residence_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_residence_id")
|
||||
}
|
||||
|
||||
daysThreshold := 30
|
||||
if d := c.Query("days_threshold"); d != "" {
|
||||
if d := c.QueryParam("days_threshold"); d != "" {
|
||||
if parsed, err := strconv.Atoi(d); err == nil {
|
||||
daysThreshold = parsed
|
||||
}
|
||||
@@ -89,352 +77,241 @@ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
|
||||
|
||||
response, err := h.taskService.GetTasksByResidence(uint(residenceID), user.ID, daysThreshold, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrResidenceAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateTask handles POST /api/tasks/
|
||||
func (h *TaskHandler) CreateTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) CreateTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
var req requests.CreateTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.taskService.CreateTask(&req, user.ID, userNow)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrResidenceAccessDenied) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateTask handles PUT/PATCH /api/tasks/:id/
|
||||
func (h *TaskHandler) UpdateTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) UpdateTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
var req requests.UpdateTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteTask handles DELETE /api/tasks/:id/
|
||||
func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) DeleteTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.DeleteTask(uint(taskID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
|
||||
func (h *TaskHandler) MarkInProgress(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) MarkInProgress(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.MarkInProgress(uint(taskID), user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CancelTask handles POST /api/tasks/:id/cancel/
|
||||
func (h *TaskHandler) CancelTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) CancelTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.CancelTask(uint(taskID), user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
case errors.Is(err, services.ErrTaskAlreadyCancelled):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_cancelled")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UncancelTask handles POST /api/tasks/:id/uncancel/
|
||||
func (h *TaskHandler) UncancelTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) UncancelTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.UncancelTask(uint(taskID), user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ArchiveTask handles POST /api/tasks/:id/archive/
|
||||
func (h *TaskHandler) ArchiveTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) ArchiveTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.ArchiveTask(uint(taskID), user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
case errors.Is(err, services.ErrTaskAlreadyArchived):
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_already_archived")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
|
||||
func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) UnarchiveTask(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// QuickComplete handles POST /api/tasks/:id/quick-complete/
|
||||
// Lightweight endpoint for widget - just returns 200 OK on success
|
||||
func (h *TaskHandler) QuickComplete(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) QuickComplete(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
err = h.taskService.QuickComplete(uint(taskID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
// === Task Completions ===
|
||||
|
||||
// GetTaskCompletions handles GET /api/tasks/:id/completions/
|
||||
func (h *TaskHandler) GetTaskCompletions(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) GetTaskCompletions(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.GetCompletionsByTask(uint(taskID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ListCompletions handles GET /api/task-completions/
|
||||
func (h *TaskHandler) ListCompletions(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) ListCompletions(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
response, err := h.taskService.ListCompletions(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetCompletion handles GET /api/task-completions/:id/
|
||||
func (h *TaskHandler) GetCompletion(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) GetCompletion(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_completion_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_completion_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.GetCompletion(uint(completionID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrCompletionNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.completion_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// CreateCompletion handles POST /api/task-completions/
|
||||
// Supports both JSON and multipart form data (for image uploads)
|
||||
func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) CreateCompletion(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
userNow := middleware.GetUserNow(c)
|
||||
|
||||
var req requests.CreateTaskCompletionRequest
|
||||
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
contentType := c.Request().Header.Get("Content-Type")
|
||||
|
||||
// Check if this is a multipart form request (image upload)
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
// Parse multipart form
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_parse_form")})
|
||||
return
|
||||
if err := c.Request().ParseMultipartForm(32 << 20); err != nil { // 32MB max
|
||||
return apperrors.BadRequest("error.failed_to_parse_form")
|
||||
}
|
||||
|
||||
// Parse task_id (required)
|
||||
taskIDStr := c.PostForm("task_id")
|
||||
taskIDStr := c.FormValue("task_id")
|
||||
if taskIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.task_id_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.task_id_required")
|
||||
}
|
||||
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id_value")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_task_id_value")
|
||||
}
|
||||
req.TaskID = uint(taskID)
|
||||
|
||||
// Parse notes (optional)
|
||||
req.Notes = c.PostForm("notes")
|
||||
req.Notes = c.FormValue("notes")
|
||||
|
||||
// Parse actual_cost (optional)
|
||||
if costStr := c.PostForm("actual_cost"); costStr != "" {
|
||||
if costStr := c.FormValue("actual_cost"); costStr != "" {
|
||||
cost, err := decimal.NewFromString(costStr)
|
||||
if err == nil {
|
||||
req.ActualCost = &cost
|
||||
@@ -442,7 +319,7 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Parse completed_at (optional)
|
||||
if completedAtStr := c.PostForm("completed_at"); completedAtStr != "" {
|
||||
if completedAtStr := c.FormValue("completed_at"); completedAtStr != "" {
|
||||
if t, err := time.Parse(time.RFC3339, completedAtStr); err == nil {
|
||||
req.CompletedAt = &t
|
||||
}
|
||||
@@ -462,87 +339,65 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
|
||||
if h.storageService != nil {
|
||||
result, err := h.storageService.Upload(file, "completions")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_upload_image")})
|
||||
return
|
||||
return apperrors.BadRequest("error.failed_to_upload_image")
|
||||
}
|
||||
req.ImageURLs = append(req.ImageURLs, result.URL)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard JSON request
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
response, err := h.taskService.CreateCompletion(&req, user.ID, userNow)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrTaskNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.task_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusCreated, response)
|
||||
return c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// DeleteCompletion handles DELETE /api/task-completions/:id/
|
||||
func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *TaskHandler) DeleteCompletion(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
completionID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_completion_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_completion_id")
|
||||
}
|
||||
|
||||
response, err := h.taskService.DeleteCompletion(uint(completionID), user.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrCompletionNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.completion_not_found")})
|
||||
case errors.Is(err, services.ErrTaskAccessDenied):
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.task_access_denied")})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// === Lookups ===
|
||||
|
||||
// GetCategories handles GET /api/tasks/categories/
|
||||
func (h *TaskHandler) GetCategories(c *gin.Context) {
|
||||
func (h *TaskHandler) GetCategories(c echo.Context) error {
|
||||
categories, err := h.taskService.GetCategories()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, categories)
|
||||
return c.JSON(http.StatusOK, categories)
|
||||
}
|
||||
|
||||
// GetPriorities handles GET /api/tasks/priorities/
|
||||
func (h *TaskHandler) GetPriorities(c *gin.Context) {
|
||||
func (h *TaskHandler) GetPriorities(c echo.Context) error {
|
||||
priorities, err := h.taskService.GetPriorities()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, priorities)
|
||||
return c.JSON(http.StatusOK, priorities)
|
||||
}
|
||||
|
||||
// GetFrequencies handles GET /api/tasks/frequencies/
|
||||
func (h *TaskHandler) GetFrequencies(c *gin.Context) {
|
||||
func (h *TaskHandler) GetFrequencies(c echo.Context) error {
|
||||
frequencies, err := h.taskService.GetFrequencies()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
c.JSON(http.StatusOK, frequencies)
|
||||
return c.JSON(http.StatusOK, frequencies)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -20,23 +20,23 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupTaskHandler(t *testing.T) (*TaskHandler, *gin.Engine, *gorm.DB) {
|
||||
func setupTaskHandler(t *testing.T) (*TaskHandler, *echo.Echo, *gorm.DB) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
taskService := services.NewTaskService(taskRepo, residenceRepo)
|
||||
handler := NewTaskHandler(taskService, nil)
|
||||
router := testutil.SetupTestRouter()
|
||||
return handler, router, db
|
||||
e := testutil.SetupTestRouter()
|
||||
return handler, e, db
|
||||
}
|
||||
|
||||
func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/", handler.CreateTask)
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
Description: "Kitchen faucet is dripping",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/tasks/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
EstimatedCost: &estimatedCost,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/tasks/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -116,29 +116,29 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
||||
Title: "Unauthorized Task",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/tasks/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskHandler_GetTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
otherUser := testutil.CreateTestUser(t, db, "other", "other@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/:id/", handler.GetTask)
|
||||
|
||||
otherGroup := router.Group("/api/other-tasks")
|
||||
otherGroup := e.Group("/api/other-tasks")
|
||||
otherGroup.Use(testutil.MockAuthMiddleware(otherUser))
|
||||
otherGroup.GET("/:id/", handler.GetTask)
|
||||
|
||||
t.Run("get own task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -151,32 +151,32 @@ func TestTaskHandler_GetTask(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("get non-existent task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/9999/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/9999/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("access denied for other user", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/other-tasks/%d/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/other-tasks/%d/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskHandler_ListTasks(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 1")
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 2")
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Task 3")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/", handler.ListTasks)
|
||||
|
||||
t.Run("list tasks", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestTaskHandler_ListTasks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_GetTasksByResidence(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
@@ -209,12 +209,12 @@ func TestTaskHandler_GetTasksByResidence(t *testing.T) {
|
||||
// Create tasks with different states
|
||||
testutil.CreateTestTask(t, db, residence.ID, user.ID, "Active Task")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/by-residence/:residence_id/", handler.GetTasksByResidence)
|
||||
|
||||
t.Run("get kanban columns", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -231,7 +231,7 @@ func TestTaskHandler_GetTasksByResidence(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("kanban column structure", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/tasks/by-residence/%d/", residence.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -254,12 +254,12 @@ func TestTaskHandler_GetTasksByResidence(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_UpdateTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Original Title")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.PUT("/:id/", handler.UpdateTask)
|
||||
|
||||
@@ -271,7 +271,7 @@ func TestTaskHandler_UpdateTask(t *testing.T) {
|
||||
Description: &newDesc,
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "PUT", fmt.Sprintf("/api/tasks/%d/", task.ID), req, "test-token")
|
||||
w := testutil.MakeRequest(e, "PUT", fmt.Sprintf("/api/tasks/%d/", task.ID), req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -290,17 +290,17 @@ func TestTaskHandler_UpdateTask(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_DeleteTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Delete")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.DELETE("/:id/", handler.DeleteTask)
|
||||
|
||||
t.Run("delete task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/tasks/%d/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -316,17 +316,17 @@ func TestTaskHandler_DeleteTask(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_CancelTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Cancel")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/cancel/", handler.CancelTask)
|
||||
|
||||
t.Run("cancel task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -344,14 +344,14 @@ func TestTaskHandler_CancelTask(t *testing.T) {
|
||||
|
||||
t.Run("cancel already cancelled task", func(t *testing.T) {
|
||||
// Already cancelled from previous test
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/cancel/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskHandler_UncancelTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Uncancel")
|
||||
@@ -360,12 +360,12 @@ func TestTaskHandler_UncancelTask(t *testing.T) {
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
taskRepo.Cancel(task.ID)
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/uncancel/", handler.UncancelTask)
|
||||
|
||||
t.Run("uncancel task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/uncancel/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/uncancel/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -383,17 +383,17 @@ func TestTaskHandler_UncancelTask(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_ArchiveTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Archive")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/archive/", handler.ArchiveTask)
|
||||
|
||||
t.Run("archive task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/archive/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/archive/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -411,7 +411,7 @@ func TestTaskHandler_ArchiveTask(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_UnarchiveTask(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Unarchive")
|
||||
@@ -420,12 +420,12 @@ func TestTaskHandler_UnarchiveTask(t *testing.T) {
|
||||
taskRepo := repositories.NewTaskRepository(db)
|
||||
taskRepo.Archive(task.ID)
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/unarchive/", handler.UnarchiveTask)
|
||||
|
||||
t.Run("unarchive task", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/unarchive/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/unarchive/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -443,18 +443,18 @@ func TestTaskHandler_UnarchiveTask(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_MarkInProgress(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Start")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/:id/mark-in-progress/", handler.MarkInProgress)
|
||||
|
||||
t.Run("mark in progress", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "POST", fmt.Sprintf("/api/tasks/%d/mark-in-progress/", task.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", fmt.Sprintf("/api/tasks/%d/mark-in-progress/", task.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -470,12 +470,12 @@ func TestTaskHandler_MarkInProgress(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_CreateCompletion(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "To Complete")
|
||||
|
||||
authGroup := router.Group("/api/task-completions")
|
||||
authGroup := e.Group("/api/task-completions")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/", handler.CreateCompletion)
|
||||
|
||||
@@ -487,7 +487,7 @@ func TestTaskHandler_CreateCompletion(t *testing.T) {
|
||||
Notes: "Completed successfully",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/task-completions/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/task-completions/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -507,7 +507,7 @@ func TestTaskHandler_CreateCompletion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_ListCompletions(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
@@ -521,12 +521,12 @@ func TestTaskHandler_ListCompletions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
authGroup := router.Group("/api/task-completions")
|
||||
authGroup := e.Group("/api/task-completions")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/", handler.ListCompletions)
|
||||
|
||||
t.Run("list completions", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/task-completions/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/task-completions/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -539,7 +539,7 @@ func TestTaskHandler_ListCompletions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_GetCompletion(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
@@ -552,12 +552,12 @@ func TestTaskHandler_GetCompletion(t *testing.T) {
|
||||
}
|
||||
db.Create(completion)
|
||||
|
||||
authGroup := router.Group("/api/task-completions")
|
||||
authGroup := e.Group("/api/task-completions")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/:id/", handler.GetCompletion)
|
||||
|
||||
t.Run("get completion", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -571,7 +571,7 @@ func TestTaskHandler_GetCompletion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_DeleteCompletion(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
|
||||
@@ -583,12 +583,12 @@ func TestTaskHandler_DeleteCompletion(t *testing.T) {
|
||||
}
|
||||
db.Create(completion)
|
||||
|
||||
authGroup := router.Group("/api/task-completions")
|
||||
authGroup := e.Group("/api/task-completions")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.DELETE("/:id/", handler.DeleteCompletion)
|
||||
|
||||
t.Run("delete completion", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "DELETE", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "DELETE", fmt.Sprintf("/api/task-completions/%d/", completion.ID), nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -604,18 +604,18 @@ func TestTaskHandler_DeleteCompletion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_GetLookups(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "user", "user@test.com", "password")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.GET("/categories/", handler.GetCategories)
|
||||
authGroup.GET("/priorities/", handler.GetPriorities)
|
||||
authGroup.GET("/frequencies/", handler.GetFrequencies)
|
||||
|
||||
t.Run("get categories", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/categories/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/categories/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -629,7 +629,7 @@ func TestTaskHandler_GetLookups(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("get priorities", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/priorities/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/priorities/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -644,7 +644,7 @@ func TestTaskHandler_GetLookups(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("get frequencies", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/frequencies/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/frequencies/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
@@ -657,12 +657,12 @@ func TestTaskHandler_GetLookups(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskHandler_JSONResponses(t *testing.T) {
|
||||
handler, router, db := setupTaskHandler(t)
|
||||
handler, e, db := setupTaskHandler(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
authGroup := router.Group("/api/tasks")
|
||||
authGroup := e.Group("/api/tasks")
|
||||
authGroup.Use(testutil.MockAuthMiddleware(user))
|
||||
authGroup.POST("/", handler.CreateTask)
|
||||
authGroup.GET("/", handler.ListTasks)
|
||||
@@ -674,7 +674,7 @@ func TestTaskHandler_JSONResponses(t *testing.T) {
|
||||
Description: "Testing JSON structure",
|
||||
}
|
||||
|
||||
w := testutil.MakeRequest(router, "POST", "/api/tasks/", req, "test-token")
|
||||
w := testutil.MakeRequest(e, "POST", "/api/tasks/", req, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusCreated)
|
||||
|
||||
@@ -714,7 +714,7 @@ func TestTaskHandler_JSONResponses(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("list response returns kanban board", func(t *testing.T) {
|
||||
w := testutil.MakeRequest(router, "GET", "/api/tasks/", nil, "test-token")
|
||||
w := testutil.MakeRequest(e, "GET", "/api/tasks/", nil, "test-token")
|
||||
|
||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
@@ -24,83 +24,74 @@ func NewTaskTemplateHandler(templateService *services.TaskTemplateService) *Task
|
||||
|
||||
// GetTemplates handles GET /api/tasks/templates/
|
||||
// Returns all active task templates as a flat list
|
||||
func (h *TaskTemplateHandler) GetTemplates(c *gin.Context) {
|
||||
func (h *TaskTemplateHandler) GetTemplates(c echo.Context) error {
|
||||
templates, err := h.templateService.GetAll()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_templates")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, templates)
|
||||
return c.JSON(http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// GetTemplatesGrouped handles GET /api/tasks/templates/grouped/
|
||||
// Returns all templates grouped by category
|
||||
func (h *TaskTemplateHandler) GetTemplatesGrouped(c *gin.Context) {
|
||||
func (h *TaskTemplateHandler) GetTemplatesGrouped(c echo.Context) error {
|
||||
grouped, err := h.templateService.GetGrouped()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_templates")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, grouped)
|
||||
return c.JSON(http.StatusOK, grouped)
|
||||
}
|
||||
|
||||
// SearchTemplates handles GET /api/tasks/templates/search/
|
||||
// Searches templates by query string
|
||||
func (h *TaskTemplateHandler) SearchTemplates(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
func (h *TaskTemplateHandler) SearchTemplates(c echo.Context) error {
|
||||
query := c.QueryParam("q")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter 'q' is required"})
|
||||
return
|
||||
return apperrors.BadRequest("error.query_required")
|
||||
}
|
||||
|
||||
if len(query) < 2 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Query must be at least 2 characters"})
|
||||
return
|
||||
return apperrors.BadRequest("error.query_too_short")
|
||||
}
|
||||
|
||||
templates, err := h.templateService.Search(query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_search_templates")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, templates)
|
||||
return c.JSON(http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// GetTemplatesByCategory handles GET /api/tasks/templates/by-category/:category_id/
|
||||
// Returns templates for a specific category
|
||||
func (h *TaskTemplateHandler) GetTemplatesByCategory(c *gin.Context) {
|
||||
func (h *TaskTemplateHandler) GetTemplatesByCategory(c echo.Context) error {
|
||||
categoryID, err := strconv.ParseUint(c.Param("category_id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_id")
|
||||
}
|
||||
|
||||
templates, err := h.templateService.GetByCategory(uint(categoryID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_templates")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, templates)
|
||||
return c.JSON(http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// GetTemplate handles GET /api/tasks/templates/:id/
|
||||
// Returns a single template by ID
|
||||
func (h *TaskTemplateHandler) GetTemplate(c *gin.Context) {
|
||||
func (h *TaskTemplateHandler) GetTemplate(c echo.Context) error {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid template ID"})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_id")
|
||||
}
|
||||
|
||||
template, err := h.templateService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.template_not_found")})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, template)
|
||||
return c.JSON(http.StatusOK, template)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ var transparentGIF, _ = base64.StdEncoding.DecodeString("R0lGODlhAQABAIAAAAAAAP/
|
||||
|
||||
// TrackEmailOpen handles email open tracking via tracking pixel
|
||||
// GET /api/track/open/:trackingID
|
||||
func (h *TrackingHandler) TrackEmailOpen(c *gin.Context) {
|
||||
func (h *TrackingHandler) TrackEmailOpen(c echo.Context) error {
|
||||
trackingID := c.Param("trackingID")
|
||||
|
||||
if trackingID != "" && h.onboardingService != nil {
|
||||
@@ -37,9 +37,9 @@ func (h *TrackingHandler) TrackEmailOpen(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Return 1x1 transparent GIF
|
||||
c.Header("Content-Type", "image/gif")
|
||||
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
c.Data(http.StatusOK, "image/gif", transparentGIF)
|
||||
c.Response().Header().Set("Content-Type", "image/gif")
|
||||
c.Response().Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
c.Response().Header().Set("Pragma", "no-cache")
|
||||
c.Response().Header().Set("Expires", "0")
|
||||
return c.Blob(http.StatusOK, "image/gif", transparentGIF)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
@@ -21,77 +21,72 @@ func NewUploadHandler(storageService *services.StorageService) *UploadHandler {
|
||||
|
||||
// UploadImage handles POST /api/uploads/image
|
||||
// Accepts multipart/form-data with "file" field
|
||||
func (h *UploadHandler) UploadImage(c *gin.Context) {
|
||||
func (h *UploadHandler) UploadImage(c echo.Context) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||
return
|
||||
return apperrors.BadRequest("error.no_file_provided")
|
||||
}
|
||||
|
||||
// Get category from query param (default: images)
|
||||
category := c.DefaultQuery("category", "images")
|
||||
category := c.QueryParam("category")
|
||||
if category == "" {
|
||||
category = "images"
|
||||
}
|
||||
|
||||
result, err := h.storageService.Upload(file, category)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// UploadDocument handles POST /api/uploads/document
|
||||
// Accepts multipart/form-data with "file" field
|
||||
func (h *UploadHandler) UploadDocument(c *gin.Context) {
|
||||
func (h *UploadHandler) UploadDocument(c echo.Context) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||
return
|
||||
return apperrors.BadRequest("error.no_file_provided")
|
||||
}
|
||||
|
||||
result, err := h.storageService.Upload(file, "documents")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// UploadCompletion handles POST /api/uploads/completion
|
||||
// For task completion photos
|
||||
func (h *UploadHandler) UploadCompletion(c *gin.Context) {
|
||||
func (h *UploadHandler) UploadCompletion(c echo.Context) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.no_file_provided")})
|
||||
return
|
||||
return apperrors.BadRequest("error.no_file_provided")
|
||||
}
|
||||
|
||||
result, err := h.storageService.Upload(file, "completions")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// DeleteFile handles DELETE /api/uploads
|
||||
// Expects JSON body with "url" field
|
||||
func (h *UploadHandler) DeleteFile(c *gin.Context) {
|
||||
func (h *UploadHandler) DeleteFile(c echo.Context) error {
|
||||
var req struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
if err := h.storageService.Delete(req.URL); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.file_deleted")})
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{"message": "File deleted successfully"})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -25,58 +25,50 @@ func NewUserHandler(userService *services.UserService) *UserHandler {
|
||||
}
|
||||
|
||||
// ListUsers handles GET /api/users/
|
||||
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *UserHandler) ListUsers(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
// Only allow listing users that share residences with the current user
|
||||
users, err := h.userService.ListUsersInSharedResidences(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"count": len(users),
|
||||
"results": users,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUser handles GET /api/users/:id/
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *UserHandler) GetUser(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
userID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_user_id")})
|
||||
return
|
||||
return apperrors.BadRequest("error.invalid_user_id")
|
||||
}
|
||||
|
||||
// Can only view users that share a residence
|
||||
targetUser, err := h.userService.GetUserIfSharedResidence(uint(userID), user.ID)
|
||||
if err != nil {
|
||||
if err == services.ErrUserNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.user_not_found")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, targetUser)
|
||||
return c.JSON(http.StatusOK, targetUser)
|
||||
}
|
||||
|
||||
// ListProfiles handles GET /api/users/profiles/
|
||||
func (h *UserHandler) ListProfiles(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *UserHandler) ListProfiles(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
// List profiles of users in shared residences
|
||||
profiles, err := h.userService.ListProfilesInSharedResidences(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"count": len(profiles),
|
||||
"results": profiles,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user