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) { return nil, errb.SysInternal("read member failed").WithCause(err) } 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) { return nil, errb.SysInternal("read member failed").WithCause(err) } } 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) } return nil, errb.SysInternal("read tenant failed").WithCause(err) } uid, err := uc.uidGen.Next(ctx, tenantID, tenant.UIDPrefix) if err != nil { return nil, errb.SysInternal("allocate uid failed").WithCause(err) } 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) } return nil, errb.SysInternal("create member failed").WithCause(err) } 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) { return nil, errb.SysInternal("create identity failed").WithCause(err) } return memberToDTO(rec), nil }