fix(plugin): add external login binding send email interface

This commit is contained in:
LinkinStar 2023-01-06 17:22:09 +08:00
parent 01c02ae2b7
commit 1db4f5586b
7 changed files with 126 additions and 34 deletions

View File

@ -4,5 +4,5 @@ import "time"
const (
ConnectorUserExternalInfoCacheKey = "answer:connector:"
ConnectorUserExternalInfoCacheTime = 1 * time.Hour
ConnectorUserExternalInfoCacheTime = 24 * time.Hour
)

View File

@ -15,8 +15,8 @@ import (
)
const (
connectorRedirectRouterPrefix = "/answer/api/v1/connector/redirect/"
connectorLoginRouterPrefix = "/answer/api/v1/connector/login/"
ConnectorRedirectRouterPrefix = "/answer/api/v1/connector/redirect/"
ConnectorLoginRouterPrefix = "/answer/api/v1/connector/login/"
)
// ConnectorController comment controller
@ -36,15 +36,6 @@ func NewConnectorController(
}
}
func (cc *ConnectorController) ConnectorRedirectRegisterRouters(r *gin.Engine) {
_ = plugin.CallConnector(func(connector plugin.Connector) error {
r.GET(connectorLoginRouterPrefix+connector.ConnectorSlugName(), cc.ConnectorRedirect(connector))
r.GET(connectorRedirectRouterPrefix+connector.ConnectorSlugName(), cc.ConnectorLogin(connector))
return nil
})
r.GET("/answer/api/v1/connector/info", cc.ConnectorsInfo)
}
func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {
return func(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
@ -53,7 +44,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn
return
}
receiverURL := fmt.Sprintf("%s%s%s", general.SiteUrl, connectorLoginRouterPrefix, connector.ConnectorSlugName())
receiverURL := fmt.Sprintf("%s%s%s", general.SiteUrl, ConnectorLoginRouterPrefix, connector.ConnectorSlugName())
redirectURL := connector.ConnectorSender(ctx, receiverURL)
if len(redirectURL) > 0 {
ctx.Redirect(http.StatusFound, redirectURL)
@ -69,7 +60,8 @@ func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn fu
ctx.Redirect(http.StatusFound, "/50x")
return
}
resp, err := cc.userExternalService.ExternalLogin(ctx, connector.ConnectorSlugName(), userInfo)
userInfo.Provider = connector.ConnectorSlugName()
resp, err := cc.userExternalService.ExternalLogin(ctx, userInfo)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
@ -78,7 +70,7 @@ func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn fu
if len(resp.AccessToken) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/index?token=%s", resp.AccessToken))
} else {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/binding?external_id=%s", resp.ExternalID))
ctx.Redirect(http.StatusFound, fmt.Sprintf("/binding?binding_key=%s", resp.BindingKey))
}
}
}
@ -95,7 +87,7 @@ func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
resp = append(resp, &schema.ConnectorInfoResp{
Name: fn.ConnectorSlugName(),
Icon: fn.ConnectorLogo(),
Link: fmt.Sprintf("%s%s%s", general.SiteUrl, connectorLoginRouterPrefix, fn.ConnectorSlugName()),
Link: fmt.Sprintf("%s%s%s", general.SiteUrl, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
})
return nil
})
@ -105,3 +97,21 @@ func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
}
handler.HandleResponse(ctx, nil, resp)
}
func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {
req := &schema.ExternalLoginBindingUserSendEmailReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
func (cc *ConnectorController) ExternalLoginBindingUser(ctx *gin.Context) {
req := &schema.ExternalLoginBindingUserReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := cc.userExternalService.ExternalLoginBindingUser(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -29,11 +29,19 @@ type Connector interface {
ConnectorReceiver(ctx *GinContext) (userInfo ExternalLoginUserInfo, err error)
}
// ExternalLoginUserInfo external login user info
type ExternalLoginUserInfo struct {
// Third party identification
// e.g. facebook, twitter, instagram
Provider string
// required. The unique user ID provided by the third-party login
ExternalID string
Name string
Email string
MetaInfo string
// optional. This name is used preferentially during registration
Name string
// optional. If email exist will bind the existing user
Email string
// optional. The original user information provided by the third-party login platform
MetaInfo string
}
var (

View File

@ -55,7 +55,7 @@ func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, externalID
// SetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
ctx context.Context, info plugin.ExternalLoginUserInfo) (err error) {
ctx context.Context, key string, info plugin.ExternalLoginUserInfo) (err error) {
cacheData, _ := json.Marshal(info)
return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+info.ExternalID,
string(cacheData), constant.ConnectorUserExternalInfoCacheTime)
@ -63,8 +63,8 @@ func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
// GetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
ctx context.Context, externalID string) (info plugin.ExternalLoginUserInfo, err error) {
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+externalID)
ctx context.Context, key string) (info plugin.ExternalLoginUserInfo, err error) {
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
if err != nil {
return info, err
}

View File

@ -2,6 +2,7 @@ package router
import (
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/plugin"
"github.com/gin-gonic/gin"
)
@ -18,5 +19,14 @@ func NewPluginAPIRouter(
}
func (pr *PluginAPIRouter) RegisterConnector(r *gin.Engine) {
pr.connectorController.ConnectorRedirectRegisterRouters(r)
connectorController := pr.connectorController
_ = plugin.CallConnector(func(connector plugin.Connector) error {
connectorSlugName := connector.ConnectorSlugName()
r.GET(controller.ConnectorLoginRouterPrefix+connectorSlugName, connectorController.ConnectorRedirect(connector))
r.GET(controller.ConnectorRedirectRouterPrefix+connectorSlugName, connectorController.ConnectorLogin(connector))
return nil
})
r.GET("/answer/api/v1/connector/info", connectorController.ConnectorsInfo)
r.POST("/answer/api/v1/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail)
r.POST("/answer/api/v1/connector/binding", connectorController.ExternalLoginBindingUser)
}

View File

@ -2,6 +2,30 @@ package schema
// UserExternalLoginResp user external login resp
type UserExternalLoginResp struct {
ExternalID string `json:"external_id"`
BindingKey string `json:"binding_key"`
AccessToken string `json:"access_token"`
}
// ExternalLoginBindingUserSendEmailReq external login binding user request
type ExternalLoginBindingUserSendEmailReq struct {
BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"`
Email string `validate:"required,gt=1,lte=512,email" json:"email"`
// If must is true, whatever email if exists, try to bind user.
// If must is false, when email exist, will only be prompted with a warning.
Must bool `json:"must"`
}
// ExternalLoginBindingUserSendEmailResp external login binding user response
type ExternalLoginBindingUserSendEmailResp struct {
EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"`
}
// ExternalLoginBindingUserReq external login binding user request
type ExternalLoginBindingUserReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
}
// ExternalLoginBindingUserResp external login binding user response
type ExternalLoginBindingUserResp struct {
AccessToken string `json:"access_token"`
}

View File

@ -4,10 +4,13 @@ import (
"context"
"time"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/plugin"
"github.com/answerdev/answer/internal/schema"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/token"
"github.com/segmentfault/pacman/errors"
)
type UserExternalLoginRepo interface {
@ -15,9 +18,9 @@ type UserExternalLoginRepo interface {
UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)
GetByExternalID(ctx context.Context, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
SetCacheUserExternalLoginInfo(
ctx context.Context, info plugin.ExternalLoginUserInfo) (err error)
ctx context.Context, key string, info plugin.ExternalLoginUserInfo) (err error)
GetCacheUserExternalLoginInfo(
ctx context.Context, externalID string) (info plugin.ExternalLoginUserInfo, err error)
ctx context.Context, key string) (info plugin.ExternalLoginUserInfo, err error)
}
// UserExternalLoginService user external login service
@ -42,15 +45,16 @@ func NewUserExternalLoginService(
// ExternalLogin if user is already a member logged in
func (us *UserExternalLoginService) ExternalLogin(
ctx context.Context, provider string, externalUserInfo plugin.ExternalLoginUserInfo) (
ctx context.Context, externalUserInfo plugin.ExternalLoginUserInfo) (
resp *schema.UserExternalLoginResp, err error) {
// cache external user info, waiting for user enter email address.
if len(externalUserInfo.Email) == 0 {
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, externalUserInfo)
bindingKey := token.GenerateToken()
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo)
if err != nil {
return nil, err
}
return &schema.UserExternalLoginResp{ExternalID: externalUserInfo.ExternalID}, nil
return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil
}
oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email)
@ -58,12 +62,12 @@ func (us *UserExternalLoginService) ExternalLogin(
return nil, err
}
if !exist {
oldUserInfo, err = us.RegisterNewUser(ctx, provider, externalUserInfo)
oldUserInfo, err = us.RegisterNewUser(ctx, externalUserInfo)
if err != nil {
return nil, err
}
}
err = us.BindOldUser(ctx, provider, externalUserInfo, oldUserInfo)
err = us.BindOldUser(ctx, externalUserInfo, oldUserInfo)
if err != nil {
return nil, err
}
@ -73,7 +77,7 @@ func (us *UserExternalLoginService) ExternalLogin(
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
func (us *UserExternalLoginService) RegisterNewUser(ctx context.Context, provider string,
func (us *UserExternalLoginService) RegisterNewUser(ctx context.Context,
externalUserInfo plugin.ExternalLoginUserInfo) (userInfo *entity.User, err error) {
userInfo = &entity.User{}
userInfo.EMail = externalUserInfo.Email
@ -92,7 +96,7 @@ func (us *UserExternalLoginService) RegisterNewUser(ctx context.Context, provide
return userInfo, nil
}
func (us *UserExternalLoginService) BindOldUser(ctx context.Context, provider string,
func (us *UserExternalLoginService) BindOldUser(ctx context.Context,
externalUserInfo plugin.ExternalLoginUserInfo, oldUserInfo *entity.User) (err error) {
oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, externalUserInfo.ExternalID)
if err != nil {
@ -105,7 +109,7 @@ func (us *UserExternalLoginService) BindOldUser(ctx context.Context, provider st
} else {
newExternalUserInfo := &entity.UserExternalLogin{
UserID: oldUserInfo.ID,
Provider: provider,
Provider: externalUserInfo.Provider,
ExternalID: externalUserInfo.ExternalID,
MetaInfo: externalUserInfo.MetaInfo,
}
@ -113,3 +117,39 @@ func (us *UserExternalLoginService) BindOldUser(ctx context.Context, provider st
}
return err
}
func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) (
resp *schema.ExternalLoginBindingUserSendEmailResp, err error) {
resp = &schema.ExternalLoginBindingUserSendEmailResp{}
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey)
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
return nil, errors.BadRequest(reason.UserNotFound)
}
_, exist, err := us.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exist && !req.Must {
resp.EmailExistAndMustBeConfirmed = true
return resp, nil
}
if !exist {
externalLoginInfo.Email = req.Email
_, err = us.RegisterNewUser(ctx, externalLoginInfo)
if err != nil {
return nil, err
}
}
// TODO send bind confirmation email
return resp, nil
}
func (us *UserExternalLoginService) ExternalLoginBindingUser(
ctx context.Context, req *schema.ExternalLoginBindingUserReq) (
resp *schema.ExternalLoginBindingUserResp, err error) {
return
}