github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/sys/verifier/impl.go (about)

     1  /*
     2   * Copyright (c) 2022-present unTill Pro, Ltd.
     3   */
     4  
     5  package verifier
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"time"
    11  
    12  	"golang.org/x/text/language"
    13  	"golang.org/x/text/message"
    14  	"golang.org/x/text/message/catalog"
    15  
    16  	"github.com/voedger/voedger/pkg/appdef"
    17  	"github.com/voedger/voedger/pkg/irates"
    18  	"github.com/voedger/voedger/pkg/istructs"
    19  	"github.com/voedger/voedger/pkg/istructsmem"
    20  	"github.com/voedger/voedger/pkg/itokens"
    21  	payloads "github.com/voedger/voedger/pkg/itokens-payloads"
    22  	"github.com/voedger/voedger/pkg/state"
    23  	"github.com/voedger/voedger/pkg/sys/smtp"
    24  	coreutils "github.com/voedger/voedger/pkg/utils"
    25  	"github.com/voedger/voedger/pkg/utils/federation"
    26  )
    27  
    28  var translationsCatalog = coreutils.GetCatalogFromTranslations(translations)
    29  
    30  // called at targetApp/profileWSID
    31  func provideQryInitiateEmailVerification(cfg *istructsmem.AppConfigType, itokens itokens.ITokens,
    32  	asp istructs.IAppStructsProvider, federation federation.IFederation) {
    33  	cfg.Resources.Add(istructsmem.NewQueryFunction(
    34  		QNameQueryInitiateEmailVerification,
    35  		provideIEVExec(cfg.Name, itokens, asp, federation),
    36  	))
    37  	// cfg.FunctionRateLimits.AddWorkspaceLimit(QNameQueryInitiateEmailVerification, istructs.RateLimit{
    38  	// 	Period:                InitiateEmailVerification_Period,
    39  	// 	MaxAllowedPerDuration: InitiateEmailVerification_MaxAllowed,
    40  	// })
    41  }
    42  
    43  // q.sys.InitiateEmailVerification
    44  // called at targetApp/profileWSID
    45  func provideIEVExec(appQName istructs.AppQName, itokens itokens.ITokens, asp istructs.IAppStructsProvider, federation federation.IFederation) istructsmem.ExecQueryClosure {
    46  	return func(ctx context.Context, args istructs.ExecQueryArgs, callback istructs.ExecQueryCallback) (err error) {
    47  		entity := args.ArgumentObject.AsString(field_Entity)
    48  		targetWSID := istructs.WSID(args.ArgumentObject.AsInt64(field_TargetWSID))
    49  		field := args.ArgumentObject.AsString(field_Field)
    50  		email := args.ArgumentObject.AsString(Field_Email)
    51  		forRegistry := args.ArgumentObject.AsBool(field_ForRegistry)
    52  		lng := args.ArgumentObject.AsString(field_Language)
    53  
    54  		as, err := asp.AppStructs(appQName)
    55  		if err != nil {
    56  			return err
    57  		}
    58  		appTokens := as.AppTokens()
    59  		if forRegistry {
    60  			// issue token for sys/registry/pseduoWSID. That's for c.sys.ResetPassword only for now
    61  			asRegistry, err := asp.AppStructs(istructs.AppQName_sys_registry)
    62  			if err != nil {
    63  				// notest
    64  				return err
    65  			}
    66  			appTokens = asRegistry.AppTokens()
    67  			targetWSID = coreutils.GetPseudoWSID(istructs.NullWSID, email, istructs.MainClusterID)
    68  		}
    69  
    70  		verificationToken, verificationCode, err := NewVerificationToken(entity, field, email, appdef.VerificationKind_EMail, targetWSID, itokens, appTokens)
    71  		if err != nil {
    72  			return err
    73  		}
    74  
    75  		systemPrincipalToken, err := payloads.GetSystemPrincipalToken(itokens, appQName)
    76  		if err != nil {
    77  			return err
    78  		}
    79  
    80  		// c.sys.SendEmailVerificationCode
    81  		body := fmt.Sprintf(`{"args":{"VerificationCode":"%s","Email":"%s","Reason":"%s","Language":"%s"}}`, verificationCode, email, verifyEmailReason, lng)
    82  		if _, err = federation.Func(fmt.Sprintf("api/%s/%d/c.sys.SendEmailVerificationCode", appQName, args.WSID), body,
    83  			coreutils.WithDiscardResponse(), coreutils.WithAuthorizeBy(systemPrincipalToken)); err != nil {
    84  			return fmt.Errorf("c.sys.SendEmailVerificationCode failed: %w", err)
    85  		}
    86  
    87  		return callback(&ievResult{verificationToken: verificationToken})
    88  	}
    89  }
    90  
    91  func applySendEmailVerificationCode(federation federation.IFederation, smtpCfg smtp.Cfg, timeFunc coreutils.TimeFunc) func(event istructs.IPLogEvent, state istructs.IState, intents istructs.IIntents) (err error) {
    92  	return func(event istructs.IPLogEvent, st istructs.IState, intents istructs.IIntents) (err error) {
    93  		eventTime := time.UnixMilli(int64(event.RegisteredAt()))
    94  		if eventTime.Add(threeDays).Before(timeFunc()) {
    95  			// skip old emails to prevent re-sending after projector rename
    96  			// see https://github.com/voedger/voedger/issues/275
    97  			return nil
    98  		}
    99  		lng := event.ArgumentObject().AsString(field_Language)
   100  
   101  		kb, err := st.KeyBuilder(state.SendMail, appdef.NullQName)
   102  		if err != nil {
   103  			return
   104  		}
   105  		reason := event.ArgumentObject().AsString(field_Reason)
   106  		translatedEmailSubject := message.NewPrinter(language.Make(lng), message.Catalog(translationsCatalog)).Sprintf(EmailSubject)
   107  		kb.PutString(state.Field_Subject, translatedEmailSubject)
   108  		kb.PutString(state.Field_To, event.ArgumentObject().AsString(Field_Email))
   109  		kb.PutString(state.Field_Body, getVerificationEmailBody(federation, event.ArgumentObject().AsString(field_VerificationCode), reason, language.Make(lng), translationsCatalog))
   110  		kb.PutString(state.Field_From, smtpCfg.GetFrom())
   111  		kb.PutString(state.Field_Host, smtpCfg.Host)
   112  		kb.PutInt32(state.Field_Port, smtpCfg.Port)
   113  		kb.PutString(state.Field_Username, smtpCfg.Username)
   114  		pwd := ""
   115  		if !coreutils.IsTest() {
   116  			kbSecret, err := st.KeyBuilder(state.AppSecret, appdef.NullQName)
   117  			if err != nil {
   118  				return err
   119  			}
   120  			kbSecret.PutString(state.Field_Secret, smtpCfg.PwdSecret)
   121  			sv, err := st.MustExist(kbSecret)
   122  			if err != nil {
   123  				return err
   124  			}
   125  			pwd = sv.AsString("")
   126  		}
   127  		kb.PutString(state.Field_Password, pwd)
   128  
   129  		_, err = intents.NewValue(kb)
   130  
   131  		return
   132  	}
   133  }
   134  
   135  func (r *ievResult) AsString(string) string {
   136  	return r.verificationToken
   137  }
   138  
   139  func (r ivvtResult) AsString(string) string {
   140  	return r.verifiedValueToken
   141  }
   142  
   143  // called at targetApp/targetWSID
   144  func provideQryIssueVerifiedValueToken(cfg *istructsmem.AppConfigType, itokens itokens.ITokens, asp istructs.IAppStructsProvider) {
   145  	cfg.Resources.Add(istructsmem.NewQueryFunction(
   146  		QNameQueryIssueVerifiedValueToken,
   147  		provideIVVTExec(itokens, cfg.Name, asp),
   148  	))
   149  
   150  	// code ok -> buckets state will be reset
   151  	cfg.FunctionRateLimits.AddWorkspaceLimit(QNameQueryIssueVerifiedValueToken, RateLimit_IssueVerifiedValueToken)
   152  }
   153  
   154  // q.sys.IssueVerifiedValueToken
   155  // called at targetApp/profileWSID
   156  // a helper is used for ResetPassword that calls `q.sys.IssueVerifiedValueToken` at the profile
   157  func provideIVVTExec(itokens itokens.ITokens, appQName istructs.AppQName, asp istructs.IAppStructsProvider) istructsmem.ExecQueryClosure {
   158  	return func(ctx context.Context, args istructs.ExecQueryArgs, callback istructs.ExecQueryCallback) (err error) {
   159  		verificationToken := args.ArgumentObject.AsString(field_VerificationToken)
   160  		verificationCode := args.ArgumentObject.AsString(field_VerificationCode)
   161  		forRegistry := args.ArgumentObject.AsBool(field_ForRegistry)
   162  
   163  		as, err := asp.AppStructs(appQName)
   164  		if err != nil {
   165  			return err
   166  		}
   167  
   168  		appTokens := as.AppTokens()
   169  		if forRegistry {
   170  			asRegistry, err := asp.AppStructs(istructs.AppQName_sys_registry)
   171  			if err != nil {
   172  				// notest
   173  				return err
   174  			}
   175  			appTokens = asRegistry.AppTokens()
   176  		}
   177  
   178  		verifiedValueToken, err := IssueVerfiedValueToken(verificationToken, verificationCode, appTokens, itokens)
   179  		if err != nil {
   180  			return err
   181  		}
   182  
   183  		// code ok -> reset per-profile rate limit
   184  		appBuckets := istructsmem.IBucketsFromIAppStructs(as)
   185  		rateLimitName := istructsmem.GetFunctionRateLimitName(QNameQueryIssueVerifiedValueToken, istructs.RateLimitKind_byWorkspace)
   186  		appBuckets.ResetRateBuckets(rateLimitName, irates.BucketState{
   187  			Period:             RateLimit_IssueVerifiedValueToken.Period,
   188  			MaxTokensPerPeriod: irates.NumTokensType(RateLimit_IssueVerifiedValueToken.MaxAllowedPerDuration),
   189  			TakenTokens:        0,
   190  		})
   191  
   192  		return callback(&ivvtResult{verifiedValueToken: verifiedValueToken})
   193  	}
   194  }
   195  
   196  func provideCmdSendEmailVerificationCode(cfg *istructsmem.AppConfigType) {
   197  	cfg.Resources.Add(istructsmem.NewCommandFunction(
   198  		QNameCommandSendEmailVerificationCode,
   199  		istructsmem.NullCommandExec,
   200  	))
   201  }
   202  
   203  func getVerificationEmailBody(federation federation.IFederation, verificationCode string, reason string, lng language.Tag, ctlg catalog.Catalog) string {
   204  	text1 := message.NewPrinter(lng, message.Catalog(ctlg)).Sprintf(`Here is your verification code`)
   205  	text2 := message.NewPrinter(lng, message.Catalog(ctlg)).Sprintf(`Please, enter this code on`)
   206  	text3 := message.NewPrinter(lng, message.Catalog(ctlg)).Sprintf(reason)
   207  	return fmt.Sprintf(`
   208  <div style="font-family: Arial, Helvetica, sans-serif;">
   209  	<div
   210  		style="margin: 20px auto 30px; width: 50%%; min-width: 200px; padding-bottom: 20px; border-bottom: 1px solid #ccc;text-align: center;">
   211  	</div>
   212  
   213  	<div style="text-align: center;">
   214  		<p style="font-size: 24px; font-weight: 300">%s</p>
   215  		<p style="font-size: 50px; font-weight: bold; text-align: center; letter-spacing: 10px; line-height: 50px; margin: 20px auto;">
   216  			%s</p>
   217  		<p>%s %s %s</p>
   218  	</div>
   219  
   220  	<div
   221  		style="color: #989898; margin: 20px auto 30px; width: 50%%; min-width: 200px; padding-top: 20px; border-top: 1px solid #ccc;text-align: center;">
   222  		%d &copy; unTill
   223  	</div>
   224  </div>
   225  `, text1, verificationCode, text2, federation.URLStr(), text3, time.Now().Year())
   226  }