Add i18n support with English and Spanish translations
- Created i18n package with translator and middleware - Added translation files for English (en.json) and Spanish (es.json) - Updated all HTTP handlers to use i18n for error/success messages - Added comprehensive test coverage for i18n (87%) - Updated CI workflow to use Go 1.24 - All tests passing with 50.8% total coverage
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"apocapoc-api/internal/application/commands"
|
||||
"apocapoc-api/internal/application/queries"
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
appErrors "apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
@@ -26,6 +27,7 @@ type AuthHandlers struct {
|
||||
jwtService *auth.JWTService
|
||||
refreshTokenRepo repositories.RefreshTokenRepository
|
||||
refreshTokenExpiry time.Duration
|
||||
translator *i18n.Translator
|
||||
}
|
||||
|
||||
func NewAuthHandlers(
|
||||
@@ -41,6 +43,7 @@ func NewAuthHandlers(
|
||||
jwtService *auth.JWTService,
|
||||
refreshTokenRepo repositories.RefreshTokenRepository,
|
||||
refreshTokenExpiry time.Duration,
|
||||
translator *i18n.Translator,
|
||||
) *AuthHandlers {
|
||||
return &AuthHandlers{
|
||||
registerHandler: registerHandler,
|
||||
@@ -55,6 +58,7 @@ func NewAuthHandlers(
|
||||
jwtService: jwtService,
|
||||
refreshTokenRepo: refreshTokenRepo,
|
||||
refreshTokenExpiry: refreshTokenExpiry,
|
||||
translator: translator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +108,7 @@ type LogoutRequest struct {
|
||||
func (h *AuthHandlers) Register(w http.ResponseWriter, r *http.Request) {
|
||||
var req RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,26 +121,27 @@ func (h *AuthHandlers) Register(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.registerHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if errors.Is(err, appErrors.ErrInvalidInput) {
|
||||
respondValidationError(w, err)
|
||||
respondValidationErrorI18n(w, r, h.translator, err)
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already registered")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusConflict, "email_already_registered")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrRegistrationClosed {
|
||||
respondError(w, http.StatusForbidden, "Registration is currently closed")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "registration_closed")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to register user")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_register_user")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
var message string
|
||||
if result.EmailVerificationRequired {
|
||||
message = "Registration successful. Please check your email to verify your account."
|
||||
message = h.translator.Success(lang, "registration_with_verification")
|
||||
} else {
|
||||
message = "Registration successful. You can now login."
|
||||
message = h.translator.Success(lang, "registration_without_verification")
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusCreated, RegisterResponse{
|
||||
@@ -161,7 +166,7 @@ func (h *AuthHandlers) Register(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *AuthHandlers) Login(w http.ResponseWriter, r *http.Request) {
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,31 +178,31 @@ func (h *AuthHandlers) Login(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.loginHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrNotFound || err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusUnauthorized, "Invalid email or password")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "invalid_credentials")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrEmailNotVerified {
|
||||
respondError(w, http.StatusForbidden, "Please verify your email before logging in")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "email_not_verified")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to login")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_login")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.jwtService.GenerateToken(result.UserID, result.Email)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to generate token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_generate_token")
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken, err := queries.CreateRefreshToken(result.UserID, h.refreshTokenExpiry)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to create refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_create_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.refreshTokenRepo.Create(r.Context(), refreshToken); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to save refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_save_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -223,7 +228,7 @@ func (h *AuthHandlers) Login(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *AuthHandlers) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
var req RefreshRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,27 +239,27 @@ func (h *AuthHandlers) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.refreshTokenHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrNotFound || err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusUnauthorized, "Invalid or expired refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "invalid_expired_refresh_token")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.jwtService.GenerateToken(result.UserID, result.Email)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to generate token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_generate_token")
|
||||
return
|
||||
}
|
||||
|
||||
newRefreshToken, err := queries.CreateRefreshToken(result.UserID, h.refreshTokenExpiry)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to create refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_create_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.refreshTokenRepo.Create(r.Context(), newRefreshToken); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to save refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_save_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -283,7 +288,7 @@ func (h *AuthHandlers) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *AuthHandlers) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
var req LogoutRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -294,19 +299,20 @@ func (h *AuthHandlers) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.revokeTokenHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Refresh token not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "refresh_token_not_found")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid refresh token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_refresh_token")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to revoke token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_refresh_token")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Successfully logged out",
|
||||
"message": h.translator.Success(lang, "logged_out"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -333,7 +339,7 @@ type ResendVerificationRequest struct {
|
||||
func (h *AuthHandlers) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
var req VerifyEmailRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -344,19 +350,20 @@ func (h *AuthHandlers) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.verifyEmailHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid or expired verification token")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_expired_verification_token")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already verified")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusConflict, "email_already_verified")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to verify email")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_verify_email")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email verified successfully",
|
||||
"message": h.translator.Success(lang, "email_verified"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -376,7 +383,7 @@ func (h *AuthHandlers) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *AuthHandlers) ResendVerification(w http.ResponseWriter, r *http.Request) {
|
||||
var req ResendVerificationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -387,23 +394,24 @@ func (h *AuthHandlers) ResendVerification(w http.ResponseWriter, r *http.Request
|
||||
err := h.resendVerificationEmailHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid email")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_email")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "user_not_found")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already verified")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusConflict, "email_already_verified")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to send verification email")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_send_verification_email")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Verification email sent successfully",
|
||||
"message": h.translator.Success(lang, "verification_email_sent"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -432,7 +440,7 @@ type ResetPasswordRequest struct {
|
||||
func (h *AuthHandlers) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
var req ForgotPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -443,23 +451,24 @@ func (h *AuthHandlers) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.requestPasswordResetHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid email")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_email")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "user_not_found")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrEmailNotVerified {
|
||||
respondError(w, http.StatusForbidden, "Please verify your email before resetting password")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "email_not_verified_reset")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to send reset email")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_send_reset_email")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Password reset email sent successfully",
|
||||
"message": h.translator.Success(lang, "password_reset_email_sent"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -478,7 +487,7 @@ func (h *AuthHandlers) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *AuthHandlers) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
var req ResetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -490,18 +499,19 @@ func (h *AuthHandlers) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.resetPasswordHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid or expired token, or password requirements not met")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_token_or_password")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "user_not_found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to reset password")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_reset_password")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Password reset successfully",
|
||||
"message": h.translator.Success(lang, "password_reset"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,14 +55,14 @@ type TodaysHabitEntryResponse struct {
|
||||
}
|
||||
|
||||
type TodaysHabitResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
ScheduledDate time.Time `json:"scheduled_date"`
|
||||
IsCarriedOver bool `json:"is_carried_over"`
|
||||
Entry *TodaysHabitEntryResponse `json:"entry,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type value_objects.HabitType `json:"type"`
|
||||
TargetValue *float64 `json:"target_value,omitempty"`
|
||||
IsNegative bool `json:"is_negative"`
|
||||
ScheduledDate time.Time `json:"scheduled_date"`
|
||||
IsCarriedOver bool `json:"is_carried_over"`
|
||||
Entry *TodaysHabitEntryResponse `json:"entry,omitempty"`
|
||||
}
|
||||
|
||||
type UserHabitResponse struct {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"apocapoc-api/internal/application/commands"
|
||||
"apocapoc-api/internal/application/queries"
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -26,6 +27,7 @@ type HabitHandlers struct {
|
||||
markHandler *commands.MarkHabitHandler
|
||||
unmarkHandler *commands.UnmarkHabitHandler
|
||||
userRepo repositories.UserRepository
|
||||
translator *i18n.Translator
|
||||
}
|
||||
|
||||
func NewHabitHandlers(
|
||||
@@ -39,6 +41,7 @@ func NewHabitHandlers(
|
||||
markHandler *commands.MarkHabitHandler,
|
||||
unmarkHandler *commands.UnmarkHabitHandler,
|
||||
userRepo repositories.UserRepository,
|
||||
translator *i18n.Translator,
|
||||
) *HabitHandlers {
|
||||
return &HabitHandlers{
|
||||
createHandler: createHandler,
|
||||
@@ -51,6 +54,7 @@ func NewHabitHandlers(
|
||||
markHandler: markHandler,
|
||||
unmarkHandler: unmarkHandler,
|
||||
userRepo: userRepo,
|
||||
translator: translator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +74,13 @@ func NewHabitHandlers(
|
||||
func (h *HabitHandlers) CreateHabit(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateHabitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -99,7 +103,7 @@ func (h *HabitHandlers) CreateHabit(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to create habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_create_habit")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +123,7 @@ func (h *HabitHandlers) CreateHabit(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *HabitHandlers) GetUserHabits(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +133,7 @@ func (h *HabitHandlers) GetUserHabits(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
habits, err := h.getUserHabitsHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get habits")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habits")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -168,7 +172,7 @@ func (h *HabitHandlers) GetHabitByID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,14 +184,14 @@ func (h *HabitHandlers) GetHabitByID(w http.ResponseWriter, r *http.Request) {
|
||||
habit, err := h.getHabitByIDHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habit")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,13 +230,13 @@ func (h *HabitHandlers) UpdateHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateHabitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -249,18 +253,18 @@ func (h *HabitHandlers) UpdateHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.updateHandler.Handle(r.Context(), cmd); err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid input")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_input")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to update habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_update_habit")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -285,7 +289,7 @@ func (h *HabitHandlers) ArchiveHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,14 +300,14 @@ func (h *HabitHandlers) ArchiveHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.archiveHandler.Handle(r.Context(), cmd); err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to archive habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_archive_habit")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,7 +337,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -345,7 +349,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
if fromStr := r.URL.Query().Get("from"); fromStr != "" {
|
||||
from, err := time.Parse("2006-01-02", fromStr)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid 'from' date format (use YYYY-MM-DD)")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_from_date_format")
|
||||
return
|
||||
}
|
||||
query.From = &from
|
||||
@@ -354,7 +358,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
if toStr := r.URL.Query().Get("to"); toStr != "" {
|
||||
to, err := time.Parse("2006-01-02", toStr)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid 'to' date format (use YYYY-MM-DD)")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_to_date_format")
|
||||
return
|
||||
}
|
||||
query.To = &to
|
||||
@@ -375,7 +379,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
if pageStr := r.URL.Query().Get("page"); pageStr != "" {
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
respondError(w, http.StatusBadRequest, "Invalid 'page' parameter")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_page_parameter")
|
||||
return
|
||||
}
|
||||
query.Page = page
|
||||
@@ -386,7 +390,7 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit < 1 || limit > 100 {
|
||||
respondError(w, http.StatusBadRequest, "Invalid 'limit' parameter (must be 1-100)")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_limit_parameter")
|
||||
return
|
||||
}
|
||||
query.Limit = limit
|
||||
@@ -402,14 +406,14 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
result, err := h.getHabitEntriesHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get habit entries")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habit_entries")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -447,13 +451,13 @@ func (h *HabitHandlers) GetHabitEntries(w http.ResponseWriter, r *http.Request)
|
||||
func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userRepo.FindByID(r.Context(), userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get user")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_user")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -473,7 +477,7 @@ func (h *HabitHandlers) GetTodaysHabits(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
habits, err := h.getTodaysHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get habits")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_habits")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -523,13 +527,13 @@ func (h *HabitHandlers) MarkHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var req MarkHabitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_request_body")
|
||||
return
|
||||
}
|
||||
|
||||
scheduledDate, err := time.Parse("2006-01-02", req.ScheduledDate)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid date format (use YYYY-MM-DD)")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_date_format")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -541,14 +545,14 @@ func (h *HabitHandlers) MarkHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.markHandler.Handle(r.Context(), cmd); err != nil {
|
||||
if err == errors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Habit already marked for this date")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusConflict, "habit_already_marked")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to mark habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_mark_habit")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -576,13 +580,13 @@ func (h *HabitHandlers) UnmarkHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
scheduledDate, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid date format (use YYYY-MM-DD)")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusBadRequest, "invalid_date_format")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -594,14 +598,14 @@ func (h *HabitHandlers) UnmarkHabit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.unmarkHandler.Handle(r.Context(), cmd); err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit entry not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_entry_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to unmark habit")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_unmark_habit")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"apocapoc-api/internal/i18n"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func respondErrorI18n(w http.ResponseWriter, r *http.Request, translator *i18n.Translator, status int, key string) {
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
message := translator.Error(lang, key)
|
||||
respondJSON(w, status, ErrorResponse{Error: message})
|
||||
}
|
||||
|
||||
func respondSuccessI18n(w http.ResponseWriter, r *http.Request, translator *i18n.Translator, key string) {
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
message := translator.Success(lang, key)
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": message})
|
||||
}
|
||||
|
||||
func respondValidationErrorI18n(w http.ResponseWriter, r *http.Request, translator *i18n.Translator, err error) {
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
errMsg := err.Error()
|
||||
var field string
|
||||
var translatedMsg string
|
||||
|
||||
if strings.Contains(errMsg, ": ") {
|
||||
parts := strings.SplitN(errMsg, ": ", 3)
|
||||
if len(parts) >= 3 {
|
||||
field = parts[1]
|
||||
validationKey := parts[2]
|
||||
translatedMsg = translator.Validation(lang, validationKey)
|
||||
respondJSON(w, http.StatusBadRequest, ValidationErrorResponse{
|
||||
Error: translatedMsg,
|
||||
Field: field,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusBadRequest, ErrorResponse{
|
||||
Error: errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
func getLanguageFromRequest(r *http.Request, translator *i18n.Translator) language.Tag {
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
return lang
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"apocapoc-api/internal/application/commands"
|
||||
"apocapoc-api/internal/application/queries"
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
"apocapoc-api/internal/infrastructure/crypto"
|
||||
"apocapoc-api/internal/infrastructure/persistence/sqlite"
|
||||
@@ -66,13 +67,15 @@ func setupTestServer(t *testing.T) *TestServer {
|
||||
|
||||
deleteUserHandler := commands.NewDeleteUserHandler(userRepo)
|
||||
|
||||
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
||||
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo)
|
||||
statsHandlers := NewStatsHandlers(getHabitStatsHandler)
|
||||
healthHandlers := NewHealthHandlers(db)
|
||||
userHandlers := NewUserHandlers(deleteUserHandler)
|
||||
translator, _ := i18n.NewTranslator()
|
||||
|
||||
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService)
|
||||
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
|
||||
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, userRepo, translator)
|
||||
statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator)
|
||||
healthHandlers := NewHealthHandlers(db)
|
||||
userHandlers := NewUserHandlers(deleteUserHandler, translator)
|
||||
|
||||
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
|
||||
|
||||
handler := http.Handler(router)
|
||||
return &TestServer{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -15,15 +16,16 @@ import (
|
||||
_ "apocapoc-api/docs"
|
||||
)
|
||||
|
||||
func NewRouter(appURL string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, userHandlers *UserHandlers, jwtService *auth.JWTService) *chi.Mux {
|
||||
func NewRouter(appURL string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, userHandlers *UserHandlers, jwtService *auth.JWTService, translator *i18n.Translator) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(i18n.LanguageMiddleware(translator))
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{appURL},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Accept-Language"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
|
||||
@@ -6,18 +6,22 @@ import (
|
||||
"apocapoc-api/internal/application/queries"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
|
||||
"apocapoc-api/internal/i18n"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type StatsHandlers struct {
|
||||
getHabitStatsHandler *queries.GetHabitStatsHandler
|
||||
translator *i18n.Translator
|
||||
}
|
||||
|
||||
func NewStatsHandlers(
|
||||
getHabitStatsHandler *queries.GetHabitStatsHandler,
|
||||
translator *i18n.Translator,
|
||||
) *StatsHandlers {
|
||||
return &StatsHandlers{
|
||||
getHabitStatsHandler: getHabitStatsHandler,
|
||||
translator: translator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +43,7 @@ func (h *StatsHandlers) GetHabitStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID, ok := GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
respondError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusUnauthorized, "user_not_authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,14 +55,14 @@ func (h *StatsHandlers) GetHabitStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := h.getHabitStatsHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Habit not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "habit_not_found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrUnauthorized {
|
||||
respondError(w, http.StatusForbidden, "Access denied")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusForbidden, "access_denied")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get habit stats")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_get_stats")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,19 @@ import (
|
||||
"net/http"
|
||||
|
||||
"apocapoc-api/internal/application/commands"
|
||||
"apocapoc-api/internal/i18n"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type UserHandlers struct {
|
||||
deleteUserHandler *commands.DeleteUserHandler
|
||||
translator *i18n.Translator
|
||||
}
|
||||
|
||||
func NewUserHandlers(deleteUserHandler *commands.DeleteUserHandler) *UserHandlers {
|
||||
func NewUserHandlers(deleteUserHandler *commands.DeleteUserHandler, translator *i18n.Translator) *UserHandlers {
|
||||
return &UserHandlers{
|
||||
deleteUserHandler: deleteUserHandler,
|
||||
translator: translator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +41,15 @@ func (h *UserHandlers) DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
err := h.deleteUserHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusNotFound, "user_not_found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to delete account")
|
||||
respondErrorI18n(w, r, h.translator, http.StatusInternalServerError, "failed_delete_user")
|
||||
return
|
||||
}
|
||||
|
||||
lang := i18n.GetLanguageFromContext(r.Context())
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Account deleted successfully",
|
||||
"message": h.translator.Success(lang, "user_deleted"),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user