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 © unTill 223 </div> 224 </div> 225 `, text1, verificationCode, text2, federation.URLStr(), text3, time.Now().Year()) 226 }