2026-05-20 23:51:22 +00:00
|
|
|
package usecase
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
member "gateway/internal/model/member/domain"
|
|
|
|
|
"gateway/internal/model/member/domain/entity"
|
|
|
|
|
"gateway/internal/model/member/domain/enum"
|
|
|
|
|
domrepo "gateway/internal/model/member/domain/repository"
|
|
|
|
|
domusecase "gateway/internal/model/member/domain/usecase"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type provisioningUseCase struct {
|
|
|
|
|
members domrepo.MemberRepository
|
|
|
|
|
identities domrepo.IdentityRepository
|
|
|
|
|
tenants domrepo.TenantRepository
|
|
|
|
|
uidGen domrepo.UIDGenerator
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProvisioningUseCaseParam wires ProvisioningUseCase.
|
|
|
|
|
type ProvisioningUseCaseParam struct {
|
|
|
|
|
Members domrepo.MemberRepository
|
|
|
|
|
Identities domrepo.IdentityRepository
|
|
|
|
|
Tenants domrepo.TenantRepository
|
|
|
|
|
UIDGen domrepo.UIDGenerator
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MustProvisioningUseCase constructs ProvisioningUseCase.
|
|
|
|
|
func MustProvisioningUseCase(param ProvisioningUseCaseParam) domusecase.ProvisioningUseCase {
|
|
|
|
|
if param.Members == nil || param.Identities == nil || param.Tenants == nil || param.UIDGen == nil {
|
|
|
|
|
panic("member: provisioning dependencies are required")
|
|
|
|
|
}
|
|
|
|
|
return &provisioningUseCase{
|
|
|
|
|
members: param.Members,
|
|
|
|
|
identities: param.Identities,
|
|
|
|
|
tenants: param.Tenants,
|
|
|
|
|
uidGen: param.UIDGen,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (uc *provisioningUseCase) EnsureFromOIDC(ctx context.Context, req *domusecase.EnsureFromOIDCRequest) (*domusecase.MemberDTO, error) {
|
|
|
|
|
if req == nil || req.TenantID == "" || req.ZitadelSub == "" {
|
|
|
|
|
return nil, errb.InputMissingRequired("tenant_id and zitadel_sub are required")
|
|
|
|
|
}
|
|
|
|
|
if existing, err := uc.members.GetByZitadelUserID(ctx, req.TenantID, req.ZitadelSub); err == nil {
|
|
|
|
|
return memberToDTO(existing), nil
|
|
|
|
|
} else if !errors.Is(err, member.ErrNotFound) {
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "read member failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
return uc.createProvisioned(ctx, req.TenantID, req.ZitadelSub, "", req.Email, req.DisplayName, req.Locale, enum.MemberOriginOIDC)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (uc *provisioningUseCase) EnsureFromLDAP(ctx context.Context, req *domusecase.EnsureFromLDAPRequest) (*domusecase.MemberDTO, error) {
|
|
|
|
|
if req == nil || req.TenantID == "" {
|
|
|
|
|
return nil, errb.InputMissingRequired("tenant_id is required")
|
|
|
|
|
}
|
|
|
|
|
sub := req.ZitadelSub
|
|
|
|
|
if sub == "" {
|
|
|
|
|
sub = req.ExternalID
|
|
|
|
|
}
|
|
|
|
|
if sub != "" {
|
|
|
|
|
if existing, err := uc.members.GetByZitadelUserID(ctx, req.TenantID, sub); err == nil {
|
|
|
|
|
return memberToDTO(existing), nil
|
|
|
|
|
} else if !errors.Is(err, member.ErrNotFound) {
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "read member failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if req.ExternalID != "" {
|
|
|
|
|
if idn, err := uc.identities.GetByExternalID(ctx, req.TenantID, req.ExternalID); err == nil {
|
|
|
|
|
rec, getErr := uc.members.GetByUID(ctx, req.TenantID, idn.UID)
|
|
|
|
|
if getErr == nil {
|
|
|
|
|
return memberToDTO(rec), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return uc.createProvisioned(ctx, req.TenantID, sub, req.ExternalID, req.Email, req.DisplayName, "", enum.MemberOriginLDAP)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (uc *provisioningUseCase) EnsureFromSCIM(ctx context.Context, req *domusecase.EnsureFromSCIMRequest) (*domusecase.MemberDTO, error) {
|
|
|
|
|
if req == nil || req.TenantID == "" {
|
|
|
|
|
return nil, errb.InputMissingRequired("tenant_id is required")
|
|
|
|
|
}
|
|
|
|
|
if req.ExternalID != "" {
|
|
|
|
|
if idn, err := uc.identities.GetByExternalID(ctx, req.TenantID, req.ExternalID); err == nil {
|
|
|
|
|
rec, getErr := uc.members.GetByUID(ctx, req.TenantID, idn.UID)
|
|
|
|
|
if getErr == nil {
|
|
|
|
|
return memberToDTO(rec), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sub := req.ZitadelSub
|
|
|
|
|
return uc.createProvisioned(ctx, req.TenantID, sub, req.ExternalID, req.Email, req.DisplayName, "", enum.MemberOriginSCIM)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (uc *provisioningUseCase) createProvisioned(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tenantID, zitadelSub, externalID, email, displayName, language string,
|
|
|
|
|
origin enum.MemberOrigin,
|
|
|
|
|
) (*domusecase.MemberDTO, error) {
|
|
|
|
|
tenant, err := uc.tenants.GetByTenantID(ctx, tenantID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, member.ErrTenantNotFound) {
|
|
|
|
|
return nil, errb.ResNotFound("tenant", tenantID).WithCause(err)
|
|
|
|
|
}
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "read tenant failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
uid, err := uc.uidGen.Next(ctx, tenantID, tenant.UIDPrefix)
|
|
|
|
|
if err != nil {
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "allocate uid failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
now := time.Now().UTC().UnixMilli()
|
|
|
|
|
rec := &entity.Member{
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
UID: uid,
|
|
|
|
|
ZitadelUserID: zitadelSub,
|
|
|
|
|
ZitadelEmail: email,
|
|
|
|
|
DisplayName: displayName,
|
|
|
|
|
Language: defaultLanguage(language),
|
|
|
|
|
Status: enum.MemberStatusActive,
|
|
|
|
|
Origin: origin,
|
|
|
|
|
CreateAt: now,
|
|
|
|
|
UpdateAt: now,
|
|
|
|
|
}
|
|
|
|
|
if err := uc.members.Insert(ctx, rec); err != nil {
|
|
|
|
|
if errors.Is(err, member.ErrDuplicateMember) {
|
|
|
|
|
if zitadelSub != "" {
|
|
|
|
|
if existing, getErr := uc.members.GetByZitadelUserID(ctx, tenantID, zitadelSub); getErr == nil {
|
|
|
|
|
return memberToDTO(existing), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, errb.ResAlreadyExist("member already exists").WithCause(err)
|
|
|
|
|
}
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "create member failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
idn := &entity.Identity{
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
UID: uid,
|
|
|
|
|
ZitadelUserID: zitadelSub,
|
|
|
|
|
ExternalID: externalID,
|
|
|
|
|
CreateAt: now,
|
|
|
|
|
UpdateAt: now,
|
|
|
|
|
}
|
|
|
|
|
if err := uc.identities.Insert(ctx, idn); err != nil && !errors.Is(err, member.ErrDuplicateMember) {
|
2026-05-21 06:45:35 +00:00
|
|
|
return nil, wrapRepoErr(err, "create identity failed")
|
2026-05-20 23:51:22 +00:00
|
|
|
}
|
|
|
|
|
return memberToDTO(rec), nil
|
|
|
|
|
}
|