274 lines
8.0 KiB
Go
274 lines
8.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"server/models"
|
|
"server/pkg/passwordutil"
|
|
)
|
|
|
|
// SyncTenantUserInput 老表 → 认证中心同步入参
|
|
type SyncTenantUserInput struct {
|
|
Tid uint64
|
|
Account string // 企业内账号
|
|
Name string
|
|
Phone string
|
|
Email string
|
|
// PasswordHash 已哈希的密码;为空表示不修改密码。
|
|
// 注意:身份已存在时,只有 UpdatePassword=true 才会生效(改密场景),
|
|
// 否则保持不动 —— 避免 B 企业建同名用户时把 A 企业的密码静默覆盖掉。
|
|
PasswordHash string
|
|
GroupID uint64
|
|
OrgID uint64
|
|
Status int8
|
|
IsDefault int8
|
|
|
|
// UpdatePassword 身份已存在时是否允许更新密码(仅改密场景传 true)
|
|
UpdatePassword bool
|
|
}
|
|
|
|
// SyncTenantUser 把租户用户同步到统一认证中心(幂等,可重复调用)。
|
|
//
|
|
// 归并规则与迁移脚本一致:手机号 > 邮箱 > 企业内账号。
|
|
// - 身份不存在 → 创建;已存在 → 更新手机/邮箱/昵称/状态
|
|
// - 绑定不存在 → 创建;已存在 → 更新账号/姓名/部门/角色/状态
|
|
//
|
|
// 注意:密码属于身份层(一人一份),因此在任一企业修改密码,
|
|
// 该用户在其他企业的登录密码会同步变化——这是统一认证的预期行为。
|
|
func SyncTenantUser(in SyncTenantUserInput) error {
|
|
_, err := SyncTenantUserAndGetIdentity(in)
|
|
return err
|
|
}
|
|
|
|
// SyncTenantUserAndGetIdentity 同 SyncTenantUser,但额外返回认证中心身份ID。
|
|
//
|
|
// 用途:新增租户用户时先把身份建出来,再用 identity_id 作为
|
|
// yz_system_tenant_user.uid——业务表(文件/客户/合同…)已在 uid 全量迁移中
|
|
// 统一成 identity_id,老表 uid 只有与它一致,业务侧才不需要任何换算。
|
|
func SyncTenantUserAndGetIdentity(in SyncTenantUserInput) (uint64, error) {
|
|
identity, err := findOrCreateIdentity(in)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := upsertTenantBinding(identity, in); err != nil {
|
|
return 0, err
|
|
}
|
|
return identity.ID, nil
|
|
}
|
|
|
|
// RemoveTenantUser 删除该用户在指定企业的绑定(保留身份本身,
|
|
// 因为该身份可能还绑定着其他企业)。
|
|
func RemoveTenantUser(tid uint64, account, phone, email string) error {
|
|
identity := findIdentityByKey(account, phone, email)
|
|
if identity == nil {
|
|
return nil // 认证中心无此身份,无需处理
|
|
}
|
|
_, err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
|
Filter("tid", tid).
|
|
Filter("identity_id", identity.ID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// findOrCreateIdentity 按归并键查找身份,不存在则创建
|
|
func findOrCreateIdentity(in SyncTenantUserInput) (*models.AuthIdentity, error) {
|
|
if m := findIdentityByKey(in.Account, in.Phone, in.Email); m != nil {
|
|
// 补全手机/邮箱/昵称;密码仅在显式授权(改密场景)时更新
|
|
update := map[string]interface{}{}
|
|
if in.Phone != "" {
|
|
update["mobile"] = in.Phone
|
|
}
|
|
if in.Email != "" {
|
|
update["email"] = in.Email
|
|
}
|
|
if in.Name != "" {
|
|
update["nickname"] = in.Name
|
|
}
|
|
if in.Status > 0 {
|
|
update["status"] = in.Status
|
|
}
|
|
if in.UpdatePassword && in.PasswordHash != "" {
|
|
update["password_hash"] = in.PasswordHash
|
|
update["password_algo"] = passwordutil.AlgoOf(in.PasswordHash)
|
|
}
|
|
if len(update) > 0 {
|
|
_, _ = models.Orm.QueryTable(new(models.AuthIdentity)).
|
|
Filter("id", m.ID).
|
|
Update(update)
|
|
}
|
|
// 重新读取,返回最新值
|
|
_ = models.Orm.QueryTable(new(models.AuthIdentity)).Filter("id", m.ID).One(m)
|
|
return m, nil
|
|
}
|
|
|
|
item := &models.AuthIdentity{
|
|
UnionID: strings.ReplaceAll(uuid.NewString(), "-", ""),
|
|
PasswordAlgo: passwordutil.AlgoLegacy,
|
|
Status: in.Status,
|
|
}
|
|
if in.Status == 0 {
|
|
item.Status = models.AuthIdentityStatusEnabled
|
|
}
|
|
if in.Phone != "" {
|
|
item.Mobile = &in.Phone
|
|
}
|
|
if in.Email != "" {
|
|
item.Email = &in.Email
|
|
}
|
|
if in.Name != "" {
|
|
item.Nickname = &in.Name
|
|
}
|
|
if in.PasswordHash != "" {
|
|
item.PasswordHash = &in.PasswordHash
|
|
item.PasswordAlgo = passwordutil.AlgoOf(in.PasswordHash)
|
|
}
|
|
id, err := models.Orm.Insert(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.ID = uint64(id)
|
|
return item, nil
|
|
}
|
|
|
|
// findIdentityByKey 按 手机 > 邮箱 > 账号 查找身份
|
|
func findIdentityByKey(account, phone, email string) *models.AuthIdentity {
|
|
base := models.Orm.QueryTable(new(models.AuthIdentity)).Filter("delete_time__isnull", true)
|
|
|
|
if v := strings.TrimSpace(phone); v != "" {
|
|
m := &models.AuthIdentity{}
|
|
if err := base.Filter("mobile", v).One(m); err == nil {
|
|
return m
|
|
}
|
|
}
|
|
if v := strings.TrimSpace(email); v != "" {
|
|
m := &models.AuthIdentity{}
|
|
if err := base.Filter("email", v).One(m); err == nil {
|
|
return m
|
|
}
|
|
}
|
|
// 企业内账号:先找绑定关系再回查身份
|
|
if v := strings.TrimSpace(account); v != "" {
|
|
var bind models.AuthTenantUser
|
|
if err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
|
Filter("account", v).
|
|
Filter("delete_time__isnull", true).
|
|
OrderBy("-is_default", "id").
|
|
One(&bind); err == nil {
|
|
m := &models.AuthIdentity{}
|
|
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
|
Filter("id", bind.IdentityID).One(m); err == nil {
|
|
return m
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// upsertTenantBinding 创建或更新身份-企业绑定
|
|
func upsertTenantBinding(identity *models.AuthIdentity, in SyncTenantUserInput) error {
|
|
var existed models.AuthTenantUser
|
|
err := models.Orm.QueryTable(new(models.AuthTenantUser)).
|
|
Filter("tid", in.Tid).
|
|
Filter("identity_id", identity.ID).
|
|
One(&existed)
|
|
|
|
if err != nil {
|
|
// 新建绑定
|
|
item := &models.AuthTenantUser{
|
|
Tid: in.Tid,
|
|
IdentityID: identity.ID,
|
|
GroupID: in.GroupID,
|
|
OrgID: in.OrgID,
|
|
Status: in.Status,
|
|
IsDefault: in.IsDefault,
|
|
}
|
|
if in.Status == 0 {
|
|
item.Status = 1
|
|
}
|
|
if in.Account != "" {
|
|
item.Account = &in.Account
|
|
}
|
|
if in.Name != "" {
|
|
item.Name = &in.Name
|
|
}
|
|
if in.Phone != "" {
|
|
item.Phone = &in.Phone
|
|
}
|
|
if in.Email != "" {
|
|
item.Email = &in.Email
|
|
}
|
|
_, err := models.Orm.Insert(item)
|
|
return err
|
|
}
|
|
|
|
update := map[string]interface{}{}
|
|
if in.Account != "" {
|
|
update["account"] = in.Account
|
|
}
|
|
if in.Name != "" {
|
|
update["name"] = in.Name
|
|
}
|
|
if in.Phone != "" {
|
|
update["phone"] = in.Phone
|
|
}
|
|
if in.Email != "" {
|
|
update["email"] = in.Email
|
|
}
|
|
if in.GroupID > 0 {
|
|
update["group_id"] = in.GroupID
|
|
}
|
|
if in.OrgID > 0 {
|
|
update["org_id"] = in.OrgID
|
|
}
|
|
if in.Status > 0 {
|
|
update["status"] = in.Status
|
|
}
|
|
if in.IsDefault >= 0 {
|
|
update["is_default"] = in.IsDefault
|
|
}
|
|
if len(update) == 0 {
|
|
return nil
|
|
}
|
|
_, err = models.Orm.QueryTable(new(models.AuthTenantUser)).
|
|
Filter("id", existed.ID).
|
|
Update(update)
|
|
return err
|
|
}
|
|
|
|
// LocateIdentityByKey 按归并键(手机 > 邮箱 > 账号)查找已有身份。
|
|
// 第二个返回值表示是否命中。供「新增租户用户」流程判断:
|
|
// 命中则要求先验证该身份的登录密码,验证通过后只建企业绑定。
|
|
func LocateIdentityByKey(account, phone, email string) (*models.AuthIdentity, bool) {
|
|
m := findIdentityByKey(account, phone, email)
|
|
return m, m != nil
|
|
}
|
|
|
|
// GetIdentityByID 按主键读取认证身份
|
|
func GetIdentityByID(id uint64) (*models.AuthIdentity, error) {
|
|
var m models.AuthIdentity
|
|
if err := models.Orm.QueryTable(new(models.AuthIdentity)).Filter("id", id).One(&m); err != nil {
|
|
return nil, err
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
// VerifyIdentityPassword 校验身份的登录密码(兼容 legacy 哈希算法)
|
|
func VerifyIdentityPassword(identity *models.AuthIdentity, plain string) bool {
|
|
if identity == nil || identity.PasswordHash == nil || *identity.PasswordHash == "" {
|
|
return false
|
|
}
|
|
return passwordutil.Verify(*identity.PasswordHash, plain)
|
|
}
|
|
|
|
// BindIdentityToTenant 把已有身份绑定到企业(只写绑定层,不改身份资料与密码)。
|
|
// 供「验证密码后绑定到新企业」流程使用,绑定层的企业内账号/姓名/联系方式独立维护。
|
|
func BindIdentityToTenant(identity *models.AuthIdentity, in SyncTenantUserInput) error {
|
|
if identity == nil {
|
|
return errors.New("身份不存在")
|
|
}
|
|
return upsertTenantBinding(identity, in)
|
|
}
|