diff --git a/apis/notice.go b/apis/notice.go index 423de6a..8e7c055 100644 --- a/apis/notice.go +++ b/apis/notice.go @@ -42,7 +42,7 @@ type Notice struct { //} func (e *Notice) Other(r *gin.RouterGroup) { - r.GET("/notice/unread", response.AuthHandler, e.Unread) + r.GET("/notice/unread", e.Unread) r.PUT("/notice/read/:id", response.AuthHandler, e.MarkRead) r.GET("/notice/read/:id", response.AuthHandler, e.Read) } @@ -122,6 +122,10 @@ func (e *Notice) MarkRead(ctx *gin.Context) { func (e *Notice) Unread(ctx *gin.Context) { api := response.Make(ctx) verify := response.VerifyHandler(ctx) + if verify == nil { + api.OK(nil) + return + } list := make([]*models.Notice, 0) err := center.Default.GetDB(ctx, &models.Notice{}).Model(&models.Notice{}). Where(&models.Notice{Read: false, UserID: verify.GetUserID()}). diff --git a/apis/user.go b/apis/user.go index 68bdbf4..275118c 100644 --- a/apis/user.go +++ b/apis/user.go @@ -2,20 +2,23 @@ package apis import ( "errors" + "fmt" "gorm.io/gorm" "net/http" + "time" "github.com/gin-gonic/gin" - "github.com/mss-boot-io/mss-boot-admin/dto" - "github.com/mss-boot-io/mss-boot-admin/middleware" - "github.com/mss-boot-io/mss-boot-admin/models" - "github.com/mss-boot-io/mss-boot-admin/pkg" "github.com/mss-boot-io/mss-boot/pkg/config/gormdb" "github.com/mss-boot-io/mss-boot/pkg/response" "github.com/mss-boot-io/mss-boot/pkg/response/actions" "github.com/mss-boot-io/mss-boot/pkg/response/controller" "github.com/mss-boot-io/mss-boot-admin/center" + "github.com/mss-boot-io/mss-boot-admin/dto" + "github.com/mss-boot-io/mss-boot-admin/middleware" + "github.com/mss-boot-io/mss-boot-admin/models" + "github.com/mss-boot-io/mss-boot-admin/notice/email" + "github.com/mss-boot-io/mss-boot-admin/pkg" "github.com/mss-boot-io/mss-boot-admin/service" ) @@ -46,6 +49,7 @@ type User struct { // Other handler func (e *User) Other(r *gin.RouterGroup) { r.POST("/user/login", middleware.Auth.LoginHandler) + r.POST("/user/fakeCaptcha", e.FakeCaptcha) r.POST("/user/login/github", middleware.Auth.LoginHandler) r.GET("/user/refresh-token", middleware.Auth.RefreshHandler) r.GET("/user/userInfo", middleware.Auth.MiddlewareFunc(), e.UserInfo) @@ -153,7 +157,88 @@ func (e *User) RefreshToken(_ *gin.Context) { // @Param data body dto.FakeCaptchaRequest true "data" // @Success 200 {object} dto.FakeCaptchaResponse // @Router /admin/api/user/fakeCaptcha [post] -func (e *User) FakeCaptcha(*gin.Context) {} +func (e *User) FakeCaptcha(ctx *gin.Context) { + api := response.Make(ctx) + req := &dto.FakeCaptchaRequest{} + if api.Bind(req).Error != nil { + api.Err(http.StatusUnprocessableEntity) + return + } + resp := &dto.FakeCaptchaResponse{} + if req.Email != "" { + // setup 01 get user by email + user := &models.User{} + err := center.Default. + GetDB(ctx, &models.User{}). + Where("email = ?", req.Email). + First(user).Error + if err != nil { + api.AddError(err) + if errors.Is(err, gorm.ErrRecordNotFound) { + api.Err(http.StatusNotFound) + return + } + api.Log.Error("GetUser error") + api.Err(http.StatusInternalServerError) + return + } + // setup 02 generate verify code + code, err := center.Default.GenerateCode(ctx, req.Email, 5*time.Minute) + if err != nil { + api.AddError(err).Log.Error("GenerateCode error") + api.Err(http.StatusInternalServerError) + return + } + // setup 03 send email + smtpHost, ok := center.GetAppConfig().GetAppConfig(ctx, "email.smtpHost") + if !ok { + api.AddError(fmt.Errorf("not support send email")). + Err(http.StatusNotImplemented) + return + } + smtpPort, ok := center.GetAppConfig().GetAppConfig(ctx, "email.smtpPort") + if !ok { + api.AddError(fmt.Errorf("not support send email")). + Err(http.StatusNotImplemented) + return + } + username, ok := center.GetAppConfig().GetAppConfig(ctx, "email.username") + if !ok { + api.AddError(fmt.Errorf("not support send email")). + Err(http.StatusNotImplemented) + return + } + password, ok := center.GetAppConfig().GetAppConfig(ctx, "email.password") + if !ok { + api.AddError(fmt.Errorf("not support send email")). + Err(http.StatusNotImplemented) + return + } + organization, ok := center.GetAppConfig().GetAppConfig(ctx, "base.websiteName") + if !ok || organization == "" { + organization = "mss-boot-io" + } + err = email.SendVerifyCode( + smtpHost, smtpPort, + username, password, + user.Username, + user.Email, + code, + organization) + if err != nil { + api.AddError(err).Log.Error("send email error") + api.Err(http.StatusInternalServerError) + return + } + + resp.Status = "ok" + api.OK(resp) + return + } + err := fmt.Errorf("not support phone") + api.AddError(err).Err(http.StatusNotImplemented) + return +} // UserInfo 获取登录用户信息 // @Summary 获取登录用户信息 diff --git a/center/default.go b/center/default.go index 05f7b0a..cb5e924 100644 --- a/center/default.go +++ b/center/default.go @@ -42,6 +42,7 @@ type DefaultCenter struct { storage.AdapterCache storage.AdapterQueue storage.AdapterLocker + VerifyCodeStoreImp } func (d *DefaultCenter) SetNotice(n NoticeImp) { @@ -112,6 +113,10 @@ func (d *DefaultCenter) SetLocker(l storage.AdapterLocker) { d.AdapterLocker = l } +func (d *DefaultCenter) SetVerifyCodeStore(v VerifyCodeStoreImp) { + d.VerifyCodeStoreImp = v +} + func (d *DefaultCenter) GetNotice() NoticeImp { return d.NoticeImp } @@ -180,6 +185,10 @@ func (d *DefaultCenter) GetLocker() storage.AdapterLocker { return d.AdapterLocker } +func (d *DefaultCenter) GetVerifyCodeStore() VerifyCodeStoreImp { + return d.VerifyCodeStoreImp +} + func (d *DefaultCenter) Stage() string { stage := os.Getenv("STAGE") if stage == "" { @@ -280,6 +289,11 @@ func SetLocker(l storage.AdapterLocker) *DefaultCenter { return Default } +func SetVerifyCodeStore(v VerifyCodeStoreImp) *DefaultCenter { + Default.SetVerifyCodeStore(v) + return Default +} + func GetNotice() NoticeImp { return Default.GetNotice() } @@ -351,3 +365,7 @@ func GetQueue() storage.AdapterQueue { func GetLocker() storage.AdapterLocker { return Default.GetLocker() } + +func GetVerifyCodeStore() VerifyCodeStoreImp { + return Default.GetVerifyCodeStore() +} diff --git a/center/type.go b/center/type.go index 72c00ed..30bf4e0 100644 --- a/center/type.go +++ b/center/type.go @@ -1,8 +1,10 @@ package center import ( + "context" + "time" + "github.com/gin-gonic/gin" - "github.com/mss-boot-io/mss-boot-admin/storage" "github.com/mss-boot-io/mss-boot/core/server" "github.com/mss-boot-io/mss-boot/pkg/config/source" "github.com/mss-boot-io/mss-boot/pkg/security" @@ -10,6 +12,8 @@ import ( "google.golang.org/grpc" "gorm.io/gorm" "gorm.io/gorm/schema" + + "github.com/mss-boot-io/mss-boot-admin/storage" ) /* @@ -36,6 +40,7 @@ type Center interface { storage.AdapterCache storage.AdapterQueue storage.AdapterLocker + VerifyCodeStoreImp } type GRPCClientImp interface { @@ -110,3 +115,8 @@ type StatisticsImp interface { NowIncrease(ctx *gin.Context, object StatisticsObject) error NowReduce(ctx *gin.Context, object StatisticsObject) error } + +type VerifyCodeStoreImp interface { + GenerateCode(ctx context.Context, key string, expire time.Duration) (string, error) + VerifyCode(ctx context.Context, key, code string) (bool, error) +} diff --git a/config/cache.go b/config/cache.go index 79e7e97..68ee3c7 100644 --- a/config/cache.go +++ b/config/cache.go @@ -51,9 +51,12 @@ func (e Cache) Init() { _redis = r.GetClient() } center.SetCache(r) + center.SetVerifyCodeStore(cache.NewVerifyCode(r)) } if e.Memory != nil { - center.SetCache(cache.NewMemory(opts...)) + m := cache.NewMemory(opts...) + center.SetCache(m) + center.SetVerifyCodeStore(cache.NewVerifyCode(m)) } if e.QueryCache && e.QueryCacheDuration > 0 && gormdb.DB != nil { cache.NewExpiration(context.Background(), e.QueryCacheDuration) diff --git a/dto/user.go b/dto/user.go index ebb7e10..1e36bb0 100644 --- a/dto/user.go +++ b/dto/user.go @@ -28,11 +28,11 @@ type LoginResponse struct { } type FakeCaptchaRequest struct { - Phone string `json:"phone" binding:"required"` + Phone string `json:"phone"` + Email string `json:"email"` } type FakeCaptchaResponse struct { - Code int8 `json:"code"` Status string `json:"status"` } diff --git a/models/user.go b/models/user.go index 7516701..68f5b7a 100644 --- a/models/user.go +++ b/models/user.go @@ -116,24 +116,35 @@ func GetUserByUsername(ctx *gin.Context, username string) (*User, error) { return &user, nil } +// GetUserByEmail get user by email +func GetUserByEmail(ctx *gin.Context, email string) (*User, error) { + var user User + err := center.GetDB(ctx, &user).Preload("Role").First(&user, "email = ?", email).Error + if err != nil { + return nil, err + } + return &user, nil +} + type UserLogin struct { - RoleID string `json:"roleID" gorm:"index;type:varchar(64)" swaggerignore:"true"` - Role *Role `json:"role" gorm:"foreignKey:RoleID;references:ID"` - PostID string `json:"postID" gorm:"index;type:varchar(64)" swaggerignore:"true"` - Post *Post `json:"post" gorm:"foreignKey:PostID;references:ID"` - DepartmentID string `json:"departmentID" gorm:"index;type:varchar(64)" swaggerignore:"true"` - Department *Department `json:"department" gorm:"foreignKey:DepartmentID;references:ID"` - Username string `json:"username" gorm:"type:varchar(20);index"` - Email string `json:"email" gorm:"type:varchar(100);index"` - Password string `json:"password,omitempty" gorm:"-"` - PasswordHash string `json:"-" gorm:"size:255;comment:密码hash" swaggerignore:"true"` - PasswordStrength string `json:"passwordStrength" gorm:"size:20;comment:密码强度"` - Salt string `json:"-" gorm:"size:255;comment:加盐" swaggerignore:"true"` - Status enum.Status `json:"status" gorm:"size:10"` - OAuth2 []*UserOAuth2 `json:"oauth2" gorm:"foreignKey:UserID;references:ID"` - Provider pkg.OAuth2Provider `json:"type" gorm:"-"` - RefreshTokenDisable bool `json:"-" gorm:"-"` - PersonAccessToken string `json:"-" gorm:"-"` + RoleID string `json:"roleID" gorm:"index;type:varchar(64)" swaggerignore:"true"` + Role *Role `json:"role" gorm:"foreignKey:RoleID;references:ID"` + PostID string `json:"postID" gorm:"index;type:varchar(64)" swaggerignore:"true"` + Post *Post `json:"post" gorm:"foreignKey:PostID;references:ID"` + DepartmentID string `json:"departmentID" gorm:"index;type:varchar(64)" swaggerignore:"true"` + Department *Department `json:"department" gorm:"foreignKey:DepartmentID;references:ID"` + Username string `json:"username" gorm:"type:varchar(20);index"` + Email string `json:"email" gorm:"type:varchar(100);index"` + Password string `json:"password,omitempty" gorm:"-"` + PasswordHash string `json:"-" gorm:"size:255;comment:密码hash" swaggerignore:"true"` + PasswordStrength string `json:"passwordStrength" gorm:"size:20;comment:密码强度"` + Salt string `json:"-" gorm:"size:255;comment:加盐" swaggerignore:"true"` + Status enum.Status `json:"status" gorm:"size:10"` + OAuth2 []*UserOAuth2 `json:"oauth2" gorm:"foreignKey:UserID;references:ID"` + Provider pkg.LoginProvider `json:"type" gorm:"-"` + RefreshTokenDisable bool `json:"-" gorm:"-"` + PersonAccessToken string `json:"-" gorm:"-"` + Captcha string `json:"captcha" gorm:"-"` } func (e *UserLogin) TableName() string { @@ -229,7 +240,7 @@ func (e *UserLogin) Verify(ctx context.Context) (bool, security.Verifier, error) defaultRole := &Role{Default: true} _ = center.GetDB(ctx.(*gin.Context), &Role{}).Where(*defaultRole).First(defaultRole).Error switch e.Provider { - case pkg.OAuth2GithubProvider: + case pkg.GithubLoginProvider: // get user from github, then add user to db // github user clientID, _ := center.GetAppConfig().GetAppConfig(c, "security.githubClientId") @@ -290,14 +301,14 @@ func (e *UserLogin) Verify(ctx context.Context) (bool, security.Verifier, error) Website: githubUser.HTMLURL, EmailVerified: true, Locale: githubUser.Location, - Provider: pkg.OAuth2GithubProvider, + Provider: pkg.GithubLoginProvider, User: &User{ UserLogin: UserLogin{ RoleID: defaultRole.ID, Username: githubUser.Email, Email: githubUser.Email, Password: e.Password, - Provider: pkg.OAuth2GithubProvider, + Provider: pkg.GithubLoginProvider, Status: enum.Enabled, }, Name: githubUser.Login, @@ -319,7 +330,7 @@ func (e *UserLogin) Verify(ctx context.Context) (bool, security.Verifier, error) userOAuth2.User.Role = defaultRole } return true, userOAuth2.User, nil - case pkg.OAuth2LarkProvider: + case pkg.LarkLoginProvider: client := http.Client{} req, err := http.NewRequest(http.MethodGet, "https://open.larksuite.com/open-apis/authen/v1/user_info", nil) if err != nil { @@ -370,14 +381,14 @@ func (e *UserLogin) Verify(ctx context.Context) (bool, security.Verifier, error) Picture: *result.Data.AvatarUrl, NickName: *result.Data.Name, EmailVerified: email != "", - Provider: pkg.OAuth2LarkProvider, + Provider: pkg.LarkLoginProvider, User: &User{ UserLogin: UserLogin{ RoleID: defaultRole.ID, Username: *result.Data.UserId, Email: email, Password: e.Password, - Provider: pkg.OAuth2LarkProvider, + Provider: pkg.LarkLoginProvider, Status: enum.Enabled, }, Name: *result.Data.Name, @@ -399,6 +410,25 @@ func (e *UserLogin) Verify(ctx context.Context) (bool, security.Verifier, error) userOAuth2.User.Role = defaultRole } return true, userOAuth2.User, nil + case pkg.EmailLoginProvider: + fmt.Println("email login", e) + // verify captcha + if e.Captcha == "" { + return false, nil, nil + } + ok, err := center.Default.VerifyCode(c, e.Email, e.Captcha) + if err != nil { + return false, nil, err + } + if !ok { + return false, nil, nil + } + // get user from db + user, err := GetUserByEmail(c, e.Email) + if err != nil { + return false, nil, err + } + return true, user, nil } // username and password user, err := GetUserByUsername(ctx.(*gin.Context), e.Username) diff --git a/models/user_oauth2.go b/models/user_oauth2.go index f8bf92c..e40703e 100644 --- a/models/user_oauth2.go +++ b/models/user_oauth2.go @@ -11,31 +11,31 @@ import "github.com/mss-boot-io/mss-boot-admin/pkg" type UserOAuth2 struct { ModelGormTenant - User *User `json:"user" gorm:"foreignKey:UserID;references:ID" swaggerignore:"true"` - UserID string `json:"user_id" gorm:"size:64"` - OpenID string `json:"openID" gorm:"size:64"` - UnionID string `json:"unionID" gorm:"column:union_id;size:64"` - Sub string `json:"sub" gorm:"size:255;comment:主题"` - Name string `json:"name" gorm:"size:255;comment:名称"` - GivenName string `json:"given_name" gorm:"size:255;comment:名"` - FamilyName string `json:"family_name" gorm:"size:255;comment:姓"` - MiddleName string `json:"middle_name" gorm:"size:255;comment:中间名"` - NickName string `json:"nickname" gorm:"size:255;comment:昵称"` - PreferredUsername string `json:"preferred_username" gorm:"size:255;comment:首选用户名"` - Profile string `json:"profile" gorm:"size:255;comment:个人资料"` - Picture string `json:"picture" gorm:"size:255;comment:图片"` - Website string `json:"website" gorm:"size:255;comment:网站"` - Email string `json:"email" gorm:"size:255;comment:邮箱"` - EmailVerified bool `json:"email_verified" gorm:"default:false;comment:邮箱是否验证"` - Gender string `json:"gender" gorm:"size:255;comment:性别"` - Birthdata string `json:"birthdata" gorm:"size:255;comment:出生日期"` - Zoneinfo string `json:"zoneinfo" gorm:"size:255;comment:时区"` - Locale string `json:"locale" gorm:"size:255;comment:语言"` - PhoneNumber string `json:"phone_number" gorm:"size:255;comment:手机号"` - PhoneNumberVerified bool `json:"phone_number_verified" gorm:"default:false;comment:手机号是否验证"` - Address string `json:"address" gorm:"size:255;comment:地址"` - EmployeeNO string `json:"employee_no" gorm:"column:employee_no;size:255;comment:员工编号"` - Provider pkg.OAuth2Provider `json:"type" gorm:"size:20;comment:登录类型"` + User *User `json:"user" gorm:"foreignKey:UserID;references:ID" swaggerignore:"true"` + UserID string `json:"user_id" gorm:"size:64"` + OpenID string `json:"openID" gorm:"size:64"` + UnionID string `json:"unionID" gorm:"column:union_id;size:64"` + Sub string `json:"sub" gorm:"size:255;comment:主题"` + Name string `json:"name" gorm:"size:255;comment:名称"` + GivenName string `json:"given_name" gorm:"size:255;comment:名"` + FamilyName string `json:"family_name" gorm:"size:255;comment:姓"` + MiddleName string `json:"middle_name" gorm:"size:255;comment:中间名"` + NickName string `json:"nickname" gorm:"size:255;comment:昵称"` + PreferredUsername string `json:"preferred_username" gorm:"size:255;comment:首选用户名"` + Profile string `json:"profile" gorm:"size:255;comment:个人资料"` + Picture string `json:"picture" gorm:"size:255;comment:图片"` + Website string `json:"website" gorm:"size:255;comment:网站"` + Email string `json:"email" gorm:"size:255;comment:邮箱"` + EmailVerified bool `json:"email_verified" gorm:"default:false;comment:邮箱是否验证"` + Gender string `json:"gender" gorm:"size:255;comment:性别"` + Birthdata string `json:"birthdata" gorm:"size:255;comment:出生日期"` + Zoneinfo string `json:"zoneinfo" gorm:"size:255;comment:时区"` + Locale string `json:"locale" gorm:"size:255;comment:语言"` + PhoneNumber string `json:"phone_number" gorm:"size:255;comment:手机号"` + PhoneNumberVerified bool `json:"phone_number_verified" gorm:"default:false;comment:手机号是否验证"` + Address string `json:"address" gorm:"size:255;comment:地址"` + EmployeeNO string `json:"employee_no" gorm:"column:employee_no;size:255;comment:员工编号"` + Provider pkg.LoginProvider `json:"type" gorm:"size:20;comment:登录类型"` } func (*UserOAuth2) TableName() string { diff --git a/notice/email/login_verify_code.html b/notice/email/login_verify_code.html new file mode 100644 index 0000000..d8c1ebe --- /dev/null +++ b/notice/email/login_verify_code.html @@ -0,0 +1,49 @@ + + +
+ +Dear User,
+Your verification code for login is:
+Please use this code to complete your login. The code is valid for 10 minutes.
+If you did not request this code, please ignore this email.
+ +Dear User,
+You have requested to reset your password. Your password reset code is:
+Please use this code to reset your password. The code is valid for 15 minutes.
+If you did not request this, please ignore this email.
+ +