From 4a38f9d92148c4e45e8e170573f0cb24f923019f Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Sat, 6 Apr 2024 15:46:57 +0800 Subject: [PATCH] Feat: send test eamil --- internal/db/user.go | 8 +++---- internal/model/user.go | 26 ++++++++++---------- internal/op/user.go | 6 +++-- server/handlers/admin.go | 51 ++++++++++++++++++++++++++-------------- server/handlers/init.go | 4 ++-- server/handlers/user.go | 13 ---------- server/model/admin.go | 15 ++++++++++++ 7 files changed, 71 insertions(+), 52 deletions(-) diff --git a/internal/db/user.go b/internal/db/user.go index d2f5feb..531bad1 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -23,7 +23,7 @@ func WithRole(role model.Role) CreateUserConfig { func WithAppendProvider(p provider.OAuth2Provider, puid string) CreateUserConfig { return func(u *model.User) { - u.UserProviders = append(u.UserProviders, model.UserProvider{ + u.UserProviders = append(u.UserProviders, &model.UserProvider{ Provider: p, ProviderUserID: puid, }) @@ -32,20 +32,20 @@ func WithAppendProvider(p provider.OAuth2Provider, puid string) CreateUserConfig func WithSetProvider(p provider.OAuth2Provider, puid string) CreateUserConfig { return func(u *model.User) { - u.UserProviders = []model.UserProvider{{ + u.UserProviders = []*model.UserProvider{{ Provider: p, ProviderUserID: puid, }} } } -func WithAppendProviders(providers []model.UserProvider) CreateUserConfig { +func WithAppendProviders(providers []*model.UserProvider) CreateUserConfig { return func(u *model.User) { u.UserProviders = append(u.UserProviders, providers...) } } -func WithSetProviders(providers []model.UserProvider) CreateUserConfig { +func WithSetProviders(providers []*model.UserProvider) CreateUserConfig { return func(u *model.User) { u.UserProviders = providers } diff --git a/internal/model/user.go b/internal/model/user.go index 27cf219..7e75670 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -42,19 +42,19 @@ type User struct { ID string `gorm:"primaryKey;type:char(32)" json:"id"` CreatedAt time.Time UpdatedAt time.Time - RegisteredByProvider bool `gorm:"not null;default:false"` - RegisteredByEmail bool `gorm:"not null;default:false"` - UserProviders []UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Username string `gorm:"not null;uniqueIndex;type:varchar(32)"` - HashedPassword []byte `gorm:"not null"` - Email string `gorm:"type:varchar(128);uniqueIndex:,where:email <> ''"` - Role Role `gorm:"not null;default:2"` - RoomUserRelations []RoomUserRelation `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` - BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + RegisteredByProvider bool `gorm:"not null;default:false"` + RegisteredByEmail bool `gorm:"not null;default:false"` + UserProviders []*UserProvider `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Username string `gorm:"not null;uniqueIndex;type:varchar(32)"` + HashedPassword []byte `gorm:"not null"` + Email string `gorm:"type:varchar(128);uniqueIndex:,where:email <> ''"` + Role Role `gorm:"not null;default:2"` + RoomUserRelations []*RoomUserRelation `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Rooms []Room `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Movies []Movie `gorm:"foreignKey:CreatorID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` + BilibiliVendor *BilibiliVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + AlistVendor []*AlistVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + EmbyVendor []*EmbyVendor `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } func (u *User) CheckPassword(password string) bool { diff --git a/internal/op/user.go b/internal/op/user.go index 3488d95..cc8eafb 100644 --- a/internal/op/user.go +++ b/internal/op/user.go @@ -316,9 +316,11 @@ func (u *User) UnbindEmail() error { return nil } +var ErrEmailUnbound = errors.New("email unbound") + func (u *User) SendTestEmail() error { if u.Email == "" { - return errors.New("unbound email") + return ErrEmailUnbound } return email.SendTestEmail(u.Username, u.Email) @@ -326,7 +328,7 @@ func (u *User) SendTestEmail() error { func (u *User) SendRetrievePasswordCaptchaEmail(host string) error { if u.Email == "" { - return errors.New("unbound email") + return ErrEmailUnbound } return email.SendRetrievePasswordCaptchaEmail(u.ID, u.Email, host) diff --git a/server/handlers/admin.go b/server/handlers/admin.go index 43eb656..fe2269f 100644 --- a/server/handlers/admin.go +++ b/server/handlers/admin.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "errors" "fmt" "net/http" "slices" @@ -12,6 +13,7 @@ import ( "github.com/maruel/natural" "github.com/sirupsen/logrus" "github.com/synctv-org/synctv/internal/db" + "github.com/synctv-org/synctv/internal/email" dbModel "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/op" "github.com/synctv-org/synctv/internal/settings" @@ -29,7 +31,6 @@ func EditAdminSettings(ctx *gin.Context) { req := model.AdminSettingsReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -261,7 +262,6 @@ func ApprovePendingUser(ctx *gin.Context) { req := model.UserIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -295,7 +295,6 @@ func BanUser(ctx *gin.Context) { req := model.UserIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -335,7 +334,6 @@ func UnBanUser(ctx *gin.Context) { req := model.UserIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -501,7 +499,6 @@ func ApprovePendingRoom(ctx *gin.Context) { req := model.RoomIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -535,7 +532,6 @@ func BanRoom(ctx *gin.Context) { req := model.RoomIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -582,7 +578,6 @@ func UnBanRoom(ctx *gin.Context) { req := model.RoomIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -616,7 +611,6 @@ func AddUser(ctx *gin.Context) { req := model.AddUserReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -643,7 +637,6 @@ func DeleteUser(ctx *gin.Context) { req := model.UserIDReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -682,7 +675,6 @@ func AdminUserPassword(ctx *gin.Context) { req := model.AdminUserPasswordReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(err.Error())) return } @@ -721,7 +713,6 @@ func AdminUsername(ctx *gin.Context) { req := model.AdminUsernameReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(err.Error())) return } @@ -760,7 +751,6 @@ func AdminRoomPassword(ctx *gin.Context) { req := model.AdminRoomPasswordReq{} if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(err.Error())) return } @@ -850,7 +840,6 @@ func AdminAddVendorBackend(ctx *gin.Context) { var req model.AddVendorBackendReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -870,7 +859,6 @@ func AdminDeleteVendorBackends(ctx *gin.Context) { var req model.VendorBackendEndpointsReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -890,7 +878,6 @@ func AdminUpdateVendorBackends(ctx *gin.Context) { var req model.AddVendorBackendReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -910,7 +897,6 @@ func AdminReconnectVendorBackends(ctx *gin.Context) { var req model.VendorBackendEndpointsReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -943,7 +929,6 @@ func AdminEnableVendorBackends(ctx *gin.Context) { var req model.VendorBackendEndpointsReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -963,7 +948,6 @@ func AdminDisableVendorBackends(ctx *gin.Context) { var req model.VendorBackendEndpointsReq if err := model.Decode(ctx, &req); err != nil { - log.WithError(err).Error("decode error") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) return } @@ -976,3 +960,34 @@ func AdminDisableVendorBackends(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } + +func SendTestEmail(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.UserEntry).Value() + log := ctx.MustGet("log").(*logrus.Entry) + + var req model.SendTestEmailReq + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + if req.Email == "" { + if err := user.SendTestEmail(); err != nil { + log.Errorf("failed to send test email: %v", err) + if errors.Is(err, op.ErrEmailUnbound) { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + } else { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + } + return + } + } else { + if err := email.SendTestEmail(user.Username, req.Email); err != nil { + log.Errorf("failed to send test email: %v", err) + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + } + + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/init.go b/server/handlers/init.go index 3286d69..9cc1300 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -79,6 +79,8 @@ func initAdmin(admin *gin.RouterGroup, root *gin.RouterGroup) { admin.POST("/settings", EditAdminSettings) + admin.POST("/email/test", SendTestEmail) + admin.GET("/vendors", AdminGetVendorBackends) admin.POST("/vendors/add", AdminAddVendorBackend) @@ -241,8 +243,6 @@ func initUser(user *gin.RouterGroup, needAuthUser *gin.RouterGroup) { needAuthUser.POST("/bind/email", UserBindEmail) needAuthUser.POST("/unbind/email", UserUnbindEmail) - - needAuthUser.POST("/bind/email/test", UserSendTestEmail) } func initVendor(vendor *gin.RouterGroup) { diff --git a/server/handlers/user.go b/server/handlers/user.go index 2f7c34b..d2dc05d 100644 --- a/server/handlers/user.go +++ b/server/handlers/user.go @@ -333,19 +333,6 @@ func UserUnbindEmail(ctx *gin.Context) { ctx.Status(http.StatusNoContent) } -func UserSendTestEmail(ctx *gin.Context) { - user := ctx.MustGet("user").(*op.UserEntry).Value() - log := ctx.MustGet("log").(*logrus.Entry) - - if err := user.SendTestEmail(); err != nil { - log.Errorf("failed to send test email: %v", err) - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - - ctx.Status(http.StatusNoContent) -} - func GetUserSignupEmailStep1Captcha(ctx *gin.Context) { log := ctx.MustGet("log").(*logrus.Entry) diff --git a/server/model/admin.go b/server/model/admin.go index 655a5cc..9016a35 100644 --- a/server/model/admin.go +++ b/server/model/admin.go @@ -175,3 +175,18 @@ func (dvbr *VendorBackendEndpointsReq) Validate() error { func (dvbr *VendorBackendEndpointsReq) Decode(ctx *gin.Context) error { return json.NewDecoder(ctx.Request.Body).Decode(dvbr) } + +type SendTestEmailReq struct { + Email string `json:"email"` +} + +func (ster *SendTestEmailReq) Validate() error { + if ster.Email != "" && !emailReg.MatchString(ster.Email) { + return errors.New("invalid email") + } + return nil +} + +func (ster *SendTestEmailReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(ster) +}