Complete rewrite of Django REST API to Go with: - Gin web framework for HTTP routing - GORM for database operations - GoAdmin for admin panel - Gorush integration for push notifications - Redis for caching and job queues Features implemented: - User authentication (login, register, logout, password reset) - Residence management (CRUD, sharing, share codes) - Task management (CRUD, kanban board, completions) - Contractor management (CRUD, specialties) - Document management (CRUD, warranties) - Notifications (preferences, push notifications) - Subscription management (tiers, limits) Infrastructure: - Docker Compose for local development - Database migrations and seed data - Admin panel for data management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
365 lines
10 KiB
Go
365 lines
10 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/treytartt/mycrib-api/internal/dto/requests"
|
|
"github.com/treytartt/mycrib-api/internal/dto/responses"
|
|
"github.com/treytartt/mycrib-api/internal/middleware"
|
|
"github.com/treytartt/mycrib-api/internal/services"
|
|
)
|
|
|
|
// AuthHandler handles authentication endpoints
|
|
type AuthHandler struct {
|
|
authService *services.AuthService
|
|
emailService *services.EmailService
|
|
cache *services.CacheService
|
|
}
|
|
|
|
// NewAuthHandler creates a new auth handler
|
|
func NewAuthHandler(authService *services.AuthService, emailService *services.EmailService, cache *services.CacheService) *AuthHandler {
|
|
return &AuthHandler{
|
|
authService: authService,
|
|
emailService: emailService,
|
|
cache: cache,
|
|
}
|
|
}
|
|
|
|
// Login handles POST /api/auth/login/
|
|
func (h *AuthHandler) Login(c *gin.Context) {
|
|
var req requests.LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
response, err := h.authService.Login(&req)
|
|
if err != nil {
|
|
status := http.StatusUnauthorized
|
|
message := "Invalid credentials"
|
|
|
|
if errors.Is(err, services.ErrUserInactive) {
|
|
message = "Account is inactive"
|
|
}
|
|
|
|
log.Debug().Err(err).Str("identifier", req.Username).Msg("Login failed")
|
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// Register handles POST /api/auth/register/
|
|
func (h *AuthHandler) Register(c *gin.Context) {
|
|
var req requests.RegisterRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
response, confirmationCode, err := h.authService.Register(&req)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
message := err.Error()
|
|
|
|
if errors.Is(err, services.ErrUsernameTaken) {
|
|
message = "Username already taken"
|
|
} else if errors.Is(err, services.ErrEmailTaken) {
|
|
message = "Email already registered"
|
|
} else {
|
|
status = http.StatusInternalServerError
|
|
message = "Registration failed"
|
|
log.Error().Err(err).Msg("Registration failed")
|
|
}
|
|
|
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
|
return
|
|
}
|
|
|
|
// Send welcome email with confirmation code (async)
|
|
if h.emailService != nil && confirmationCode != "" {
|
|
go func() {
|
|
if err := h.emailService.SendWelcomeEmail(req.Email, req.FirstName, confirmationCode); err != nil {
|
|
log.Error().Err(err).Str("email", req.Email).Msg("Failed to send welcome email")
|
|
}
|
|
}()
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, response)
|
|
}
|
|
|
|
// Logout handles POST /api/auth/logout/
|
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
token := middleware.GetAuthToken(c)
|
|
if token == "" {
|
|
c.JSON(http.StatusUnauthorized, responses.ErrorResponse{Error: "Not authenticated"})
|
|
return
|
|
}
|
|
|
|
// Invalidate token in database
|
|
if err := h.authService.Logout(token); err != nil {
|
|
log.Warn().Err(err).Msg("Failed to delete token from database")
|
|
}
|
|
|
|
// Invalidate token in cache
|
|
if h.cache != 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: "Logged out successfully"})
|
|
}
|
|
|
|
// CurrentUser handles GET /api/auth/me/
|
|
func (h *AuthHandler) CurrentUser(c *gin.Context) {
|
|
user := middleware.MustGetAuthUser(c)
|
|
if user == nil {
|
|
return
|
|
}
|
|
|
|
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: "Failed to get user"})
|
|
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
|
|
}
|
|
|
|
var req requests.UpdateProfileRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
response, err := h.authService.UpdateProfile(user.ID, &req)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrEmailTaken) {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{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: "Failed to update profile"})
|
|
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
|
|
}
|
|
|
|
var req requests.VerifyEmailRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
err := h.authService.VerifyEmail(user.ID, req.Code)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
message := err.Error()
|
|
|
|
if errors.Is(err, services.ErrInvalidCode) {
|
|
message = "Invalid verification code"
|
|
} else if errors.Is(err, services.ErrCodeExpired) {
|
|
message = "Verification code has expired"
|
|
} else if errors.Is(err, services.ErrAlreadyVerified) {
|
|
message = "Email already verified"
|
|
} else {
|
|
status = http.StatusInternalServerError
|
|
message = "Verification failed"
|
|
log.Error().Err(err).Uint("user_id", user.ID).Msg("Email verification failed")
|
|
}
|
|
|
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
|
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
|
|
}
|
|
|
|
code, err := h.authService.ResendVerificationCode(user.ID)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrAlreadyVerified) {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{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: "Failed to resend verification"})
|
|
return
|
|
}
|
|
|
|
// Send verification email (async)
|
|
if h.emailService != nil {
|
|
go func() {
|
|
if err := h.emailService.SendVerificationEmail(user.Email, user.FirstName, code); err != nil {
|
|
log.Error().Err(err).Str("email", user.Email).Msg("Failed to send verification email")
|
|
}
|
|
}()
|
|
}
|
|
|
|
c.JSON(http.StatusOK, responses.MessageResponse{Message: "Verification email sent"})
|
|
}
|
|
|
|
// ForgotPassword handles POST /api/auth/forgot-password/
|
|
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|
var req requests.ForgotPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
code, user, err := h.authService.ForgotPassword(req.Email)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrRateLimitExceeded) {
|
|
c.JSON(http.StatusTooManyRequests, responses.ErrorResponse{
|
|
Error: "Too many password reset requests. Please try again later.",
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Error().Err(err).Str("email", req.Email).Msg("Forgot password failed")
|
|
// Don't reveal errors to prevent email enumeration
|
|
}
|
|
|
|
// Send password reset email (async) - only if user found
|
|
if h.emailService != nil && code != "" && user != nil {
|
|
go func() {
|
|
if err := h.emailService.SendPasswordResetEmail(user.Email, user.FirstName, code); err != nil {
|
|
log.Error().Err(err).Str("email", user.Email).Msg("Failed to send password reset email")
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Always return success to prevent email enumeration
|
|
c.JSON(http.StatusOK, responses.ForgotPasswordResponse{
|
|
Message: "If an account with that email exists, a password reset code has been sent.",
|
|
})
|
|
}
|
|
|
|
// VerifyResetCode handles POST /api/auth/verify-reset-code/
|
|
func (h *AuthHandler) VerifyResetCode(c *gin.Context) {
|
|
var req requests.VerifyResetCodeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
resetToken, err := h.authService.VerifyResetCode(req.Email, req.Code)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
message := "Invalid verification code"
|
|
|
|
if errors.Is(err, services.ErrCodeExpired) {
|
|
message = "Verification code has expired"
|
|
} else if errors.Is(err, services.ErrRateLimitExceeded) {
|
|
status = http.StatusTooManyRequests
|
|
message = "Too many attempts. Please request a new code."
|
|
}
|
|
|
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, responses.VerifyResetCodeResponse{
|
|
Message: "Code verified successfully",
|
|
ResetToken: resetToken,
|
|
})
|
|
}
|
|
|
|
// ResetPassword handles POST /api/auth/reset-password/
|
|
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|
var req requests.ResetPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, responses.ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: map[string]string{
|
|
"validation": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
err := h.authService.ResetPassword(req.ResetToken, req.NewPassword)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
message := "Invalid or expired reset token"
|
|
|
|
if errors.Is(err, services.ErrInvalidResetToken) {
|
|
message = "Invalid or expired reset token"
|
|
} else {
|
|
status = http.StatusInternalServerError
|
|
message = "Password reset failed"
|
|
log.Error().Err(err).Msg("Password reset failed")
|
|
}
|
|
|
|
c.JSON(status, responses.ErrorResponse{Error: message})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, responses.ResetPasswordResponse{
|
|
Message: "Password reset successfully. Please log in with your new password.",
|
|
})
|
|
}
|