package auth import ( "context" "errors" "strings" errs "gateway/internal/library/errors" "gateway/internal/library/zitadel" authmetaenum "gateway/internal/model/auth/domain/enum" domauth "gateway/internal/model/auth/domain/usecase" memberenum "gateway/internal/model/member/domain/enum" dommember "gateway/internal/model/member/domain/usecase" "gateway/internal/svc" "gateway/internal/types" "github.com/zeromicro/go-zero/core/logx" ) func resolveTenant(ctx context.Context, sc *svc.ServiceContext, slug string) (*dommember.TenantDTO, error) { if sc.MemberTenant == nil { return nil, errb.SysNotImplemented("member tenant not configured") } slug = strings.TrimSpace(slug) tenant, err := sc.MemberTenant.ResolveBySlug(ctx, slug) if err != nil { return nil, err } if tenant.Status != memberenum.TenantStatusActive.String() { return nil, errb.AuthForbidden("tenant registration is not allowed") } return tenant, nil } func wrapZitadelErr(err error) error { if err == nil { return nil } if errors.Is(err, zitadel.ErrNotConfigured) { return errb.SysNotImplemented("zitadel not configured").WithCause(err) } if errors.Is(err, zitadel.ErrUserAlreadyExists) { return errb.ResAlreadyExist("email already registered").WithCause(err) } if errors.Is(err, zitadel.ErrInvalidCredentials) { return errb.AuthUnauthorized("invalid credentials").WithCause(err) } if errors.Is(err, zitadel.ErrInvalidIDToken) { return errb.AuthUnauthorized("invalid id_token").WithCause(err) } if e := errs.FromError(err); e != nil { return err } return errb.SvcThirdParty("zitadel request failed").WithCause(err) } func registrationPurpose() memberenum.OTPPurpose { return memberenum.OTPPurposeRegistrationEmail } func recordRegistrationMeta( ctx context.Context, sc *svc.ServiceContext, tenantID, uid, inviteCodeID, acceptTermsVersion string, marketingOptIn bool, channel authmetaenum.RegistrationChannel, ) error { if sc.AuthRegistrationMeta == nil { return errb.SysNotImplemented("registration metadata not configured") } meta := RequestMetaFromContext(ctx) return sc.AuthRegistrationMeta.Record(ctx, &domauth.RecordRegistrationRequest{ TenantID: tenantID, UID: uid, InviteCodeID: inviteCodeID, AcceptTermsVersion: acceptTermsVersion, MarketingOptIn: marketingOptIn, Channel: channel, ClientIP: strings.TrimSpace(meta.ClientIP), UserAgent: strings.TrimSpace(meta.UserAgent), }) } func requireRegistrationDeps(sc *svc.ServiceContext) error { if sc.Zitadel == nil { return errb.SysNotImplemented("zitadel not configured") } if sc.MemberLifecycle == nil { return errb.SysNotImplemented("member lifecycle not configured") } if sc.MemberProfile == nil { return errb.SysNotImplemented("member profile not configured") } if sc.MemberOTP == nil { return errb.SysNotImplemented("member OTP not configured") } if sc.MemberVerifyRate == nil { return errb.SysNotImplemented("member verify rate not configured") } if sc.Notifier == nil { return errb.SysNotImplemented("notifier not configured") } return nil } func resumeRegistration( ctx context.Context, sc *svc.ServiceContext, tenantSlug, email string, ) (*types.RegisterData, error) { tenant, err := resolveTenant(ctx, sc, tenantSlug) if err != nil { return nil, err } email = normalizeLoginEmail(email) member, err := sc.MemberProfile.GetByZitadelEmail(ctx, tenant.TenantID, email) if err != nil { if isMemberNotFound(err) { return nil, errb.ResNotFound("member", email) } return nil, err } if member.Status != memberenum.MemberStatusUnverified { return nil, errb.ResInvalidState("account already verified, please login") } data, err := sendRegistrationOTP(ctx, sc, tenant.TenantID, member.UID, email) if err != nil { return nil, err } data.UID = member.UID return data, nil } func recoverPendingRegistration( ctx context.Context, sc *svc.ServiceContext, tenant *dommember.TenantDTO, req *types.RegisterReq, ) (*types.RegisterData, error) { if req == nil { return nil, errb.InputMissingRequired("request body is required") } email := normalizeLoginEmail(req.Email) tok, err := sc.Zitadel.VerifyPassword(ctx, email, req.Password) if err != nil { return nil, errb.AuthUnauthorized("invalid credentials").WithCause(wrapZitadelErr(err)) } identity, err := zitadelIdentityFromToken(ctx, sc.Zitadel, tok) if err != nil { return nil, err } memberDTO, err := memberForRegistrationRecovery(ctx, sc, tenant.TenantID, identity.Sub, email, req) if err != nil { return nil, err } switch memberDTO.Status { case memberenum.MemberStatusUnverified: case memberenum.MemberStatusActive: return nil, errb.ResAlreadyExist("email already registered, please login") default: return nil, errb.ResInvalidState("account cannot complete registration") } data, err := sendRegistrationOTP(ctx, sc, tenant.TenantID, memberDTO.UID, email) if err != nil { return nil, err } data.UID = memberDTO.UID return data, nil } func memberForRegistrationRecovery( ctx context.Context, sc *svc.ServiceContext, tenantID, zitadelSub, email string, req *types.RegisterReq, ) (*dommember.MemberDTO, error) { if dto, err := sc.MemberProfile.GetByZitadelUserID(ctx, tenantID, zitadelSub); err == nil { return dto, nil } else if !isMemberNotFound(err) { return nil, err } if dto, err := sc.MemberProfile.GetByZitadelEmail(ctx, tenantID, email); err == nil { return dto, nil } else if !isMemberNotFound(err) { return nil, err } memberDTO, err := sc.MemberLifecycle.CreateUnverified(ctx, &dommember.CreatePlatformMemberRequest{ TenantID: tenantID, Email: email, DisplayName: strings.TrimSpace(req.DisplayName), Language: strings.TrimSpace(req.Language), ZitadelUserID: zitadelSub, }) if err != nil { return nil, err } if err := recordRegistrationMeta(ctx, sc, tenantID, memberDTO.UID, "", req.AcceptTermsVersion, req.MarketingOptIn, authmetaenum.RegistrationChannelEmail); err != nil { logx.WithContext(ctx).Infof("register recover: registration meta skipped: %v", err) } return memberDTO, nil }