package handler import ( "context" "github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/pkg/response" middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // UserHandler handles user-related requests type UserHandler struct { userService *service.UserService emailService *service.EmailService emailCache service.EmailCache } // NewUserHandler creates a new UserHandler func NewUserHandler(userService *service.UserService, emailService *service.EmailService, emailCache service.EmailCache) *UserHandler { return &UserHandler{ userService: userService, emailService: emailService, emailCache: emailCache, } } // ChangePasswordRequest represents the change password request payload type ChangePasswordRequest struct { OldPassword string `json:"old_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=6"` } // UpdateProfileRequest represents the update profile request payload type UpdateProfileRequest struct { Username *string `json:"username"` AvatarURL *string `json:"avatar_url"` BalanceNotifyEnabled *bool `json:"balance_notify_enabled"` BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` } type userProfileResponse struct { dto.User AvatarURL string `json:"avatar_url,omitempty"` Identities service.UserIdentitySummarySet `json:"identities"` } // GetProfile handles getting user profile // GET /api/v1/users/me func (h *UserHandler) GetProfile(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } userData, err := h.userService.GetProfile(c.Request.Context(), subject.UserID) if err != nil { response.ErrorFrom(c, err) return } profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, userData) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, profileResp) } // ChangePassword handles changing user password // POST /api/v1/users/me/password func (h *UserHandler) ChangePassword(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req ChangePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } svcReq := service.ChangePasswordRequest{ CurrentPassword: req.OldPassword, NewPassword: req.NewPassword, } err := h.userService.ChangePassword(c.Request.Context(), subject.UserID, svcReq) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": "Password changed successfully"}) } // UpdateProfile handles updating user profile // PUT /api/v1/users/me func (h *UserHandler) UpdateProfile(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req UpdateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } svcReq := service.UpdateProfileRequest{ Username: req.Username, AvatarURL: req.AvatarURL, BalanceNotifyEnabled: req.BalanceNotifyEnabled, BalanceNotifyThreshold: req.BalanceNotifyThreshold, } updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq) if err != nil { response.ErrorFrom(c, err) return } profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, profileResp) } type StartIdentityBindingRequest struct { Provider string `json:"provider" binding:"required"` RedirectTo string `json:"redirect_to"` } // StartIdentityBinding returns the backend authorize URL for starting a third-party identity bind flow. // POST /api/v1/user/auth-identities/bind/start func (h *UserHandler) StartIdentityBinding(c *gin.Context) { if _, ok := middleware2.GetAuthSubjectFromContext(c); !ok { response.Unauthorized(c, "User not authenticated") return } var req StartIdentityBindingRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } result, err := h.userService.PrepareIdentityBindingStart(c.Request.Context(), service.StartUserIdentityBindingRequest{ Provider: req.Provider, RedirectTo: req.RedirectTo, }) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, result) } // SendNotifyEmailCodeRequest represents the request to send notify email verification code type SendNotifyEmailCodeRequest struct { Email string `json:"email" binding:"required,email"` } // SendNotifyEmailCode sends verification code to extra notification email // POST /api/v1/user/notify-email/send-code func (h *UserHandler) SendNotifyEmailCode(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req SendNotifyEmailCodeRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } err := h.userService.SendNotifyEmailCode(c.Request.Context(), subject.UserID, req.Email, h.emailService, h.emailCache) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": "Verification code sent successfully"}) } // VerifyNotifyEmailRequest represents the request to verify and add notify email type VerifyNotifyEmailRequest struct { Email string `json:"email" binding:"required,email"` Code string `json:"code" binding:"required,len=6"` } // VerifyNotifyEmail verifies code and adds email to notification list // POST /api/v1/user/notify-email/verify func (h *UserHandler) VerifyNotifyEmail(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req VerifyNotifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } err := h.userService.VerifyAndAddNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Code, h.emailCache) if err != nil { response.ErrorFrom(c, err) return } // Return updated user updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID) if err != nil { response.ErrorFrom(c, err) return } profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, profileResp) } // RemoveNotifyEmailRequest represents the request to remove a notify email type RemoveNotifyEmailRequest struct { Email string `json:"email" binding:"required,email"` } // RemoveNotifyEmail removes email from notification list // DELETE /api/v1/user/notify-email func (h *UserHandler) RemoveNotifyEmail(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req RemoveNotifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } err := h.userService.RemoveNotifyEmail(c.Request.Context(), subject.UserID, req.Email) if err != nil { response.ErrorFrom(c, err) return } // Return updated user updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID) if err != nil { response.ErrorFrom(c, err) return } profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, profileResp) } // ToggleNotifyEmailRequest represents the request to toggle a notify email's disabled state type ToggleNotifyEmailRequest struct { Email string `json:"email" binding:"required,email"` Disabled bool `json:"disabled"` } // ToggleNotifyEmail toggles the disabled state of a notification email // PUT /api/v1/user/notify-email/toggle func (h *UserHandler) ToggleNotifyEmail(c *gin.Context) { subject, ok := middleware2.GetAuthSubjectFromContext(c) if !ok { response.Unauthorized(c, "User not authenticated") return } var req ToggleNotifyEmailRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } err := h.userService.ToggleNotifyEmail(c.Request.Context(), subject.UserID, req.Email, req.Disabled) if err != nil { response.ErrorFrom(c, err) return } updatedUser, err := h.userService.GetByID(c.Request.Context(), subject.UserID) if err != nil { response.ErrorFrom(c, err) return } profileResp, err := h.buildUserProfileResponse(c.Request.Context(), subject.UserID, updatedUser) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, profileResp) } func (h *UserHandler) buildUserProfileResponse(ctx context.Context, userID int64, user *service.User) (userProfileResponse, error) { identities, err := h.userService.GetProfileIdentitySummaries(ctx, userID, user) if err != nil { return userProfileResponse{}, err } return userProfileResponseFromService(user, identities), nil } func userProfileResponseFromService(user *service.User, identities service.UserIdentitySummarySet) userProfileResponse { base := dto.UserFromService(user) if base == nil { return userProfileResponse{} } return userProfileResponse{ User: *base, AvatarURL: user.AvatarURL, Identities: identities, } }