2026-05-21 06:45:35 +00:00
|
|
|
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"
|
2026-05-26 16:55:37 +00:00
|
|
|
"gateway/internal/types"
|
|
|
|
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/logx"
|
2026-05-21 06:45:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
2026-05-26 16:55:37 +00:00
|
|
|
if sc.MemberProfile == nil {
|
|
|
|
|
return errb.SysNotImplemented("member profile not configured")
|
|
|
|
|
}
|
2026-05-21 06:45:35 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-26 16:55:37 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|