eac039bba4
- Introduced OIDC configuration options in config.example.yaml. - Added OIDC client initialization and routes for OIDC login, callback, and exchange. - Implemented OIDC login service to handle user authentication via OIDC. - Created frontend components for OIDC login and callback handling. - Updated user creation logic to support OIDC users and their identities. - Enhanced error handling for OIDC login processes.
247 lines
7.3 KiB
Go
247 lines
7.3 KiB
Go
package services
|
|
|
|
import (
|
|
"cs-agent/internal/models"
|
|
"cs-agent/internal/pkg/config"
|
|
"cs-agent/internal/pkg/dto/response"
|
|
"cs-agent/internal/pkg/enums"
|
|
"cs-agent/internal/pkg/errorsx"
|
|
"cs-agent/internal/repositories"
|
|
"cs-agent/internal/wxwork"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mlogclub/simple/common/jsons"
|
|
"github.com/mlogclub/simple/common/strs"
|
|
"github.com/mlogclub/simple/sqls"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var WxWorkLoginService = &wxWorkLoginService{}
|
|
|
|
type wxWorkLoginService struct {
|
|
}
|
|
|
|
func (s *wxWorkLoginService) BuildWxWorkLoginURL(next string) (string, error) {
|
|
if !wxwork.Enabled() {
|
|
return "", errorsx.BusinessError(1, "企业微信登录未启用")
|
|
}
|
|
state, err := wxwork.CreateState(next)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return wxwork.BuildLoginURL(state)
|
|
}
|
|
|
|
func (s *wxWorkLoginService) BuildWxWorkQRCodeLoginURL(next string) (string, error) {
|
|
if !wxwork.Enabled() {
|
|
return "", errorsx.BusinessError(1, "企业微信登录未启用")
|
|
}
|
|
state, err := wxwork.CreateState(next)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return wxwork.BuildQRCodeLoginURL(state)
|
|
}
|
|
|
|
func (s *wxWorkLoginService) LoginByWxWork(code, state string, authCfg config.AuthConfig, clientIP, userAgent string) (string, string, error) {
|
|
next, err := wxwork.ParseState(state)
|
|
if err != nil {
|
|
return "", "", errorsx.Unauthorized("企业微信登录状态无效或已过期")
|
|
}
|
|
profile, err := wxwork.GetUserDetail(code)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
loginResp, err := s.loginWithWxWorkProfile(profile, authCfg, clientIP, userAgent)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
ticket, err := wxwork.IssueLoginTicket(loginResp)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return ticket, next, nil
|
|
}
|
|
|
|
func (s *wxWorkLoginService) ExchangeWxWorkLoginTicket(ticket string) (*response.LoginResponse, error) {
|
|
return wxwork.ConsumeLoginTicket(ticket)
|
|
}
|
|
|
|
func (s *wxWorkLoginService) loginWithWxWorkProfile(profile *wxwork.LoginUser, authCfg config.AuthConfig, clientIP, userAgent string) (*response.LoginResponse, error) {
|
|
if profile == nil || strings.TrimSpace(profile.UserID) == "" {
|
|
return nil, errorsx.BusinessError(2, "企业微信用户信息不存在")
|
|
}
|
|
|
|
var ret *response.LoginResponse
|
|
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
|
var (
|
|
identity = repositories.UserIdentityRepository.GetBy(ctx.Tx, enums.ThirdProviderWxWork, profile.CorpID, profile.UserID)
|
|
user *models.User
|
|
err error
|
|
)
|
|
if identity == nil {
|
|
user, identity, err = s.createWxWorkUser(ctx, profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if identity.Status != enums.StatusOk {
|
|
return errorsx.BusinessError(3, "当前企业微信绑定已停用")
|
|
}
|
|
user = repositories.UserRepository.Get(ctx.Tx, identity.UserID)
|
|
if user == nil {
|
|
return errorsx.BusinessError(4, "企业微信账号绑定的系统用户不存在")
|
|
}
|
|
}
|
|
|
|
if user.Status != enums.StatusOk {
|
|
return errorsx.Unauthorized("当前系统账号已被禁用")
|
|
}
|
|
|
|
if err = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{
|
|
"nickname": s.resolveWxWorkNickname(user.Nickname, profile),
|
|
"avatar": s.resolveWxWorkAvatar(user.Avatar, profile),
|
|
"last_login_at": time.Now(),
|
|
"last_login_ip": clientIP,
|
|
"update_user_id": user.ID,
|
|
"update_user_name": user.Username,
|
|
"updated_at": time.Now(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = repositories.UserIdentityRepository.Updates(ctx.Tx, identity.ID, map[string]any{
|
|
"raw_profile": jsons.ToJsonStr(profile),
|
|
"last_auth_at": time.Now(),
|
|
"status": enums.StatusOk,
|
|
"update_user_id": user.ID,
|
|
"update_user_name": user.Username,
|
|
"updated_at": time.Now(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
ret, err = AuthService.issueTokens(ctx, user, clientIP, userAgent, authCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (s *wxWorkLoginService) createWxWorkUser(ctx *sqls.TxContext, profile *wxwork.LoginUser) (*models.User, *models.UserIdentity, error) {
|
|
username := strings.TrimSpace(profile.UserID)
|
|
mobile := strings.TrimSpace(profile.Mobile)
|
|
email := strings.TrimSpace(s.firstNonEmpty(profile.Email, profile.BizMail))
|
|
now := time.Now()
|
|
|
|
if err := s.checkWxWorkProfile(ctx.Tx, username, mobile, email); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
user := &models.User{
|
|
Username: username,
|
|
Nickname: s.resolveWxWorkNickname("", profile),
|
|
Avatar: s.resolveWxWorkAvatar("", profile),
|
|
Password: "",
|
|
PasswordSalt: "",
|
|
Status: enums.StatusOk,
|
|
AuditFields: models.AuditFields{
|
|
CreatedAt: now,
|
|
CreateUserID: 0,
|
|
CreateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderWxWork),
|
|
UpdatedAt: now,
|
|
UpdateUserID: 0,
|
|
UpdateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderWxWork),
|
|
},
|
|
}
|
|
if err := repositories.UserRepository.Create(ctx.Tx, user); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
identity := &models.UserIdentity{
|
|
UserID: user.ID,
|
|
Provider: enums.ThirdProviderWxWork,
|
|
ProviderUserID: strings.TrimSpace(profile.UserID),
|
|
ProviderCorpID: strings.TrimSpace(profile.CorpID),
|
|
ProviderName: enums.GetThirdProviderLabel(enums.ThirdProviderWxWork),
|
|
RawProfile: jsons.ToJsonStr(profile),
|
|
Status: enums.StatusOk,
|
|
LastAuthAt: &now,
|
|
AuditFields: models.AuditFields{
|
|
CreatedAt: now,
|
|
CreateUserID: user.ID,
|
|
CreateUserName: user.Username,
|
|
UpdatedAt: now,
|
|
UpdateUserID: user.ID,
|
|
UpdateUserName: user.Username,
|
|
},
|
|
}
|
|
if unionID := strings.TrimSpace(profile.OpenID); unionID != "" {
|
|
identity.ProviderUnionID = &unionID
|
|
}
|
|
if err := repositories.UserIdentityRepository.Create(ctx.Tx, identity); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return user, identity, nil
|
|
}
|
|
|
|
func (s *wxWorkLoginService) resolveWxWorkNickname(current string, profile *wxwork.LoginUser) string {
|
|
if profile != nil {
|
|
if name := strings.TrimSpace(profile.Name); name != "" {
|
|
return name
|
|
}
|
|
}
|
|
if current = strings.TrimSpace(current); current != "" {
|
|
return current
|
|
}
|
|
if profile != nil {
|
|
return strings.TrimSpace(profile.UserID)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *wxWorkLoginService) resolveWxWorkAvatar(current string, profile *wxwork.LoginUser) string {
|
|
if profile != nil {
|
|
if avatar := strings.TrimSpace(profile.Avatar); avatar != "" {
|
|
return avatar
|
|
}
|
|
}
|
|
return strings.TrimSpace(current)
|
|
}
|
|
|
|
func (s *wxWorkLoginService) checkWxWorkProfile(tx *gorm.DB, username, mobile string, email string) error {
|
|
if strs.IsBlank(username) {
|
|
return errorsx.BusinessError(5, "企业微信用户ID获取失败")
|
|
}
|
|
if existing := repositories.UserRepository.GetByUsername(tx, username); existing != nil {
|
|
return errorsx.BusinessError(5, "企业微信用户ID已被系统用户名占用")
|
|
}
|
|
if strs.IsNotBlank(mobile) {
|
|
if repositories.UserRepository.GetByMobile(tx, mobile) != nil {
|
|
return errorsx.BusinessError(6, "企业微信手机号已被系统用户占用")
|
|
}
|
|
}
|
|
if strs.IsNotBlank(email) {
|
|
if repositories.UserRepository.GetByEmail(tx, email) != nil {
|
|
return errorsx.BusinessError(7, "企业微信邮箱已被系统用户占用")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *wxWorkLoginService) firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|