Improve email verification flow and error handling
- Send verification email before creating user to prevent orphaned accounts - Detect SMTP authentication errors and fail fast without retries - Add field-level validation errors for better frontend UX - Configure docker-compose with explicit environment variables - Document dollar sign escaping in .env.example (use $$) - Implement welcome email on successful verification - Clean up unnecessary comments
This commit is contained in:
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -9,18 +10,22 @@ import (
|
||||
"apocapoc-api/internal/application/queries"
|
||||
"apocapoc-api/internal/domain/repositories"
|
||||
"apocapoc-api/internal/infrastructure/auth"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
appErrors "apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type AuthHandlers struct {
|
||||
registerHandler *commands.RegisterUserHandler
|
||||
loginHandler *queries.LoginUserHandler
|
||||
refreshTokenHandler *queries.RefreshTokenHandler
|
||||
revokeTokenHandler *commands.RevokeTokenHandler
|
||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
||||
jwtService *auth.JWTService
|
||||
refreshTokenRepo repositories.RefreshTokenRepository
|
||||
refreshTokenExpiry time.Duration
|
||||
registerHandler *commands.RegisterUserHandler
|
||||
loginHandler *queries.LoginUserHandler
|
||||
refreshTokenHandler *queries.RefreshTokenHandler
|
||||
revokeTokenHandler *commands.RevokeTokenHandler
|
||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler
|
||||
verifyEmailHandler *commands.VerifyEmailHandler
|
||||
resendVerificationEmailHandler *commands.ResendVerificationEmailHandler
|
||||
requestPasswordResetHandler *commands.RequestPasswordResetHandler
|
||||
resetPasswordHandler *commands.ResetPasswordHandler
|
||||
jwtService *auth.JWTService
|
||||
refreshTokenRepo repositories.RefreshTokenRepository
|
||||
refreshTokenExpiry time.Duration
|
||||
}
|
||||
|
||||
func NewAuthHandlers(
|
||||
@@ -29,19 +34,27 @@ func NewAuthHandlers(
|
||||
refreshTokenHandler *queries.RefreshTokenHandler,
|
||||
revokeTokenHandler *commands.RevokeTokenHandler,
|
||||
revokeAllTokensHandler *commands.RevokeAllTokensHandler,
|
||||
verifyEmailHandler *commands.VerifyEmailHandler,
|
||||
resendVerificationEmailHandler *commands.ResendVerificationEmailHandler,
|
||||
requestPasswordResetHandler *commands.RequestPasswordResetHandler,
|
||||
resetPasswordHandler *commands.ResetPasswordHandler,
|
||||
jwtService *auth.JWTService,
|
||||
refreshTokenRepo repositories.RefreshTokenRepository,
|
||||
refreshTokenExpiry time.Duration,
|
||||
) *AuthHandlers {
|
||||
return &AuthHandlers{
|
||||
registerHandler: registerHandler,
|
||||
loginHandler: loginHandler,
|
||||
refreshTokenHandler: refreshTokenHandler,
|
||||
revokeTokenHandler: revokeTokenHandler,
|
||||
revokeAllTokensHandler: revokeAllTokensHandler,
|
||||
jwtService: jwtService,
|
||||
refreshTokenRepo: refreshTokenRepo,
|
||||
refreshTokenExpiry: refreshTokenExpiry,
|
||||
registerHandler: registerHandler,
|
||||
loginHandler: loginHandler,
|
||||
refreshTokenHandler: refreshTokenHandler,
|
||||
revokeTokenHandler: revokeTokenHandler,
|
||||
revokeAllTokensHandler: revokeAllTokensHandler,
|
||||
verifyEmailHandler: verifyEmailHandler,
|
||||
resendVerificationEmailHandler: resendVerificationEmailHandler,
|
||||
requestPasswordResetHandler: requestPasswordResetHandler,
|
||||
resetPasswordHandler: resetPasswordHandler,
|
||||
jwtService: jwtService,
|
||||
refreshTokenRepo: refreshTokenRepo,
|
||||
refreshTokenExpiry: refreshTokenExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,15 +116,15 @@ func (h *AuthHandlers) Register(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result, err := h.registerHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == errors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid email or password (min 8 characters)")
|
||||
if errors.Is(err, appErrors.ErrInvalidInput) {
|
||||
respondValidationError(w, err)
|
||||
return
|
||||
}
|
||||
if err == errors.ErrAlreadyExists {
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already registered")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrRegistrationClosed {
|
||||
if err == appErrors.ErrRegistrationClosed {
|
||||
respondError(w, http.StatusForbidden, "Registration is currently closed")
|
||||
return
|
||||
}
|
||||
@@ -159,11 +172,11 @@ func (h *AuthHandlers) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result, err := h.loginHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound || err == errors.ErrInvalidInput {
|
||||
if err == appErrors.ErrNotFound || err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusUnauthorized, "Invalid email or password")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrEmailNotVerified {
|
||||
if err == appErrors.ErrEmailNotVerified {
|
||||
respondError(w, http.StatusForbidden, "Please verify your email before logging in")
|
||||
return
|
||||
}
|
||||
@@ -220,7 +233,7 @@ func (h *AuthHandlers) Refresh(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
result, err := h.refreshTokenHandler.Handle(r.Context(), query)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound || err == errors.ErrInvalidInput {
|
||||
if err == appErrors.ErrNotFound || err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusUnauthorized, "Invalid or expired refresh token")
|
||||
return
|
||||
}
|
||||
@@ -280,11 +293,11 @@ func (h *AuthHandlers) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
err := h.revokeTokenHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "Refresh token not found")
|
||||
return
|
||||
}
|
||||
if err == errors.ErrInvalidInput {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid refresh token")
|
||||
return
|
||||
}
|
||||
@@ -296,3 +309,199 @@ func (h *AuthHandlers) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
"message": "Successfully logged out",
|
||||
})
|
||||
}
|
||||
|
||||
type VerifyEmailRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ResendVerificationRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// VerifyEmail godoc
|
||||
// @Summary Verify email address
|
||||
// @Description Verify user email address using the token sent via email
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body VerifyEmailRequest true "Verification token"
|
||||
// @Success 200 {object} map[string]string "Email verified successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid or expired token"
|
||||
// @Failure 409 {object} ErrorResponse "Email already verified"
|
||||
// @Failure 500 {object} ErrorResponse "Internal server error"
|
||||
// @Router /auth/verify-email [post]
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := commands.VerifyEmailCommand{
|
||||
Token: req.Token,
|
||||
}
|
||||
|
||||
err := h.verifyEmailHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid or expired verification token")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already verified")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to verify email")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email verified successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ResendVerification godoc
|
||||
// @Summary Resend verification email
|
||||
// @Description Resend the email verification link to the user's email address
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ResendVerificationRequest true "User email"
|
||||
// @Success 200 {object} map[string]string "Verification email sent"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid email"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 409 {object} ErrorResponse "Email already verified"
|
||||
// @Failure 500 {object} ErrorResponse "Internal server error"
|
||||
// @Router /auth/resend-verification [post]
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := commands.ResendVerificationEmailCommand{
|
||||
Email: req.Email,
|
||||
}
|
||||
|
||||
err := h.resendVerificationEmailHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid email")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrAlreadyExists {
|
||||
respondError(w, http.StatusConflict, "Email already verified")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to send verification email")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Verification email sent successfully",
|
||||
})
|
||||
}
|
||||
|
||||
type ForgotPasswordRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
Token string `json:"token"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// ForgotPassword godoc
|
||||
// @Summary Request password reset
|
||||
// @Description Request a password reset email with a reset token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ForgotPasswordRequest true "User email"
|
||||
// @Success 200 {object} map[string]string "Reset email sent successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid email"
|
||||
// @Failure 403 {object} ErrorResponse "Email not verified"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Internal server error"
|
||||
// @Router /auth/forgot-password [post]
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := commands.RequestPasswordResetCommand{
|
||||
Email: req.Email,
|
||||
}
|
||||
|
||||
err := h.requestPasswordResetHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == appErrors.ErrInvalidInput {
|
||||
respondError(w, http.StatusBadRequest, "Invalid email")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrEmailNotVerified {
|
||||
respondError(w, http.StatusForbidden, "Please verify your email before resetting password")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to send reset email")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Password reset email sent successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword godoc
|
||||
// @Summary Reset password
|
||||
// @Description Reset user password using the reset token from email
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ResetPasswordRequest true "Reset token and new password"
|
||||
// @Success 200 {object} map[string]string "Password reset successfully"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid token or password requirements not met"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Internal server error"
|
||||
// @Router /auth/reset-password [post]
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := commands.ResetPasswordCommand{
|
||||
Token: req.Token,
|
||||
NewPassword: req.NewPassword,
|
||||
}
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
if err == appErrors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to reset password")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Password reset successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,5 +85,6 @@ type HabitEntriesResponse struct {
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Error string `json:"error"`
|
||||
Field *string `json:"field,omitempty"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/application/commands"
|
||||
@@ -590,3 +591,22 @@ func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
func respondError(w http.ResponseWriter, status int, message string) {
|
||||
respondJSON(w, status, ErrorResponse{Error: message})
|
||||
}
|
||||
|
||||
func respondValidationError(w http.ResponseWriter, err error) {
|
||||
errMsg := err.Error()
|
||||
var field *string
|
||||
|
||||
if strings.Contains(errMsg, ": ") {
|
||||
parts := strings.SplitN(errMsg, ": ", 3)
|
||||
if len(parts) >= 3 {
|
||||
fieldName := parts[1]
|
||||
field = &fieldName
|
||||
errMsg = parts[2]
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusBadRequest, ErrorResponse{
|
||||
Error: errMsg,
|
||||
Field: field,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,12 +40,17 @@ func setupTestServer(t *testing.T) *TestServer {
|
||||
habitRepo := sqlite.NewHabitRepository(db)
|
||||
entryRepo := sqlite.NewHabitEntryRepository(db)
|
||||
refreshTokenRepo := sqlite.NewRefreshTokenRepository(db)
|
||||
passwordResetTokenRepo := sqlite.NewPasswordResetTokenRepository(db)
|
||||
|
||||
registerHandler := commands.NewRegisterUserHandler(userRepo, passwordHasher, nil, "", "open")
|
||||
loginHandler := queries.NewLoginUserHandler(userRepo, passwordHasher)
|
||||
refreshTokenHandler := queries.NewRefreshTokenHandler(refreshTokenRepo, userRepo)
|
||||
revokeTokenHandler := commands.NewRevokeTokenHandler(refreshTokenRepo)
|
||||
revokeAllTokensHandler := commands.NewRevokeAllTokensHandler(refreshTokenRepo)
|
||||
verifyEmailHandler := commands.NewVerifyEmailHandler(userRepo)
|
||||
resendVerificationEmailHandler := commands.NewResendVerificationEmailHandler(userRepo, nil, "")
|
||||
requestPasswordResetHandler := commands.NewRequestPasswordResetHandler(userRepo, passwordResetTokenRepo, nil, "")
|
||||
resetPasswordHandler := commands.NewResetPasswordHandler(userRepo, passwordResetTokenRepo, passwordHasher)
|
||||
createHandler := commands.NewCreateHabitHandler(habitRepo)
|
||||
getTodaysHandler := queries.NewGetTodaysHabitsHandler(habitRepo, entryRepo)
|
||||
getUserHabitsHandler := queries.NewGetUserHabitsHandler(habitRepo)
|
||||
@@ -59,12 +64,15 @@ func setupTestServer(t *testing.T) *TestServer {
|
||||
|
||||
refreshTokenExpiry := 7 * 24 * time.Hour
|
||||
|
||||
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, jwtService, refreshTokenRepo, refreshTokenExpiry)
|
||||
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)
|
||||
statsHandlers := NewStatsHandlers(getHabitStatsHandler)
|
||||
healthHandlers := NewHealthHandlers(db)
|
||||
userHandlers := NewUserHandlers(deleteUserHandler)
|
||||
|
||||
router := NewRouter("*", habitHandlers, authHandlers, statsHandlers, healthHandlers, jwtService)
|
||||
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService)
|
||||
|
||||
handler := http.Handler(router)
|
||||
return &TestServer{
|
||||
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
_ "apocapoc-api/docs"
|
||||
)
|
||||
|
||||
func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, jwtService *auth.JWTService) *chi.Mux {
|
||||
func NewRouter(appURL string, habitHandlers *HabitHandlers, authHandlers *AuthHandlers, statsHandlers *StatsHandlers, healthHandlers *HealthHandlers, userHandlers *UserHandlers, jwtService *auth.JWTService) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{corsOrigins},
|
||||
AllowedOrigins: []string{appURL},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||
AllowCredentials: true,
|
||||
@@ -42,6 +42,10 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
||||
r.Post("/login", authHandlers.Login)
|
||||
r.Post("/refresh", authHandlers.Refresh)
|
||||
r.Post("/logout", authHandlers.Logout)
|
||||
r.Post("/verify-email", authHandlers.VerifyEmail)
|
||||
r.Post("/resend-verification", authHandlers.ResendVerification)
|
||||
r.Post("/forgot-password", authHandlers.ForgotPassword)
|
||||
r.Post("/reset-password", authHandlers.ResetPassword)
|
||||
})
|
||||
|
||||
r.Route("/api/v1/habits", func(r chi.Router) {
|
||||
@@ -65,5 +69,11 @@ func NewRouter(corsOrigins string, habitHandlers *HabitHandlers, authHandlers *A
|
||||
r.Get("/habits/{id}", statsHandlers.GetHabitStats)
|
||||
})
|
||||
|
||||
r.Route("/api/v1/users", func(r chi.Router) {
|
||||
r.Use(AuthMiddleware(jwtService))
|
||||
r.Use(RateLimitByUser(jwtService, 100, 1*time.Minute))
|
||||
r.Delete("/me", userHandlers.DeleteAccount)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apocapoc-api/internal/application/commands"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type UserHandlers struct {
|
||||
deleteUserHandler *commands.DeleteUserHandler
|
||||
}
|
||||
|
||||
func NewUserHandlers(deleteUserHandler *commands.DeleteUserHandler) *UserHandlers {
|
||||
return &UserHandlers{
|
||||
deleteUserHandler: deleteUserHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAccount godoc
|
||||
// @Summary Delete user account
|
||||
// @Description Permanently delete the authenticated user's account and all associated data (habits, entries, tokens). This action cannot be undone.
|
||||
// @Tags users
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string "Account deleted successfully"
|
||||
// @Failure 401 {object} ErrorResponse "Unauthorized - invalid or missing token"
|
||||
// @Failure 404 {object} ErrorResponse "User not found"
|
||||
// @Failure 500 {object} ErrorResponse "Internal server error"
|
||||
// @Router /users/me [delete]
|
||||
func (h *UserHandlers) DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Context().Value("user_id").(string)
|
||||
|
||||
cmd := commands.DeleteUserCommand{
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
err := h.deleteUserHandler.Handle(r.Context(), cmd)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotFound {
|
||||
respondError(w, http.StatusNotFound, "User not found")
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusInternalServerError, "Failed to delete account")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Account deleted successfully",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user