Skip to content

Commit

Permalink
Support sending test SMS
Browse files Browse the repository at this point in the history
ref DEV-2301
  • Loading branch information
louischan-oursky committed Jan 23, 2025
2 parents e0e0118 + 2a0c21c commit 775ddcd
Show file tree
Hide file tree
Showing 21 changed files with 944 additions and 41 deletions.
27 changes: 27 additions & 0 deletions pkg/lib/infra/sms/custom/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/authgear/authgear-server/pkg/lib/hook"
"github.com/authgear/authgear-server/pkg/lib/infra/sms/smsapi"
utilhttputil "github.com/authgear/authgear-server/pkg/util/httputil"
"github.com/authgear/authgear-server/pkg/util/log"
)

type SMSHookTimeout struct {
Expand Down Expand Up @@ -51,6 +52,14 @@ func NewHookDenoClient(endpoint config.DenoEndpoint, logger hook.Logger, timeout
}
}

func NewSMSWebHook(hook hook.WebHook, smsCfg *config.CustomSMSProviderConfig) *SMSWebHook {
httpClient := NewHookHTTPClient(NewSMSHookTimeout(smsCfg))
return &SMSWebHook{
WebHook: hook,
Client: httpClient,
}
}

type SMSWebHook struct {
hook.WebHook
Client HookHTTPClient
Expand Down Expand Up @@ -82,6 +91,15 @@ func (w *SMSWebHook) Call(ctx context.Context, u *url.URL, payload SendOptions)
}
}

func NewSMSDenoHook(lf *log.Factory, denoEndpoint config.DenoEndpoint, smsCfg *config.CustomSMSProviderConfig) *SMSDenoHook {
timeout := NewSMSHookTimeout(smsCfg)
logger := hook.NewLogger(lf)
client := NewHookDenoClient(denoEndpoint, logger, timeout)
return &SMSDenoHook{
Client: client,
}
}

type SMSDenoHook struct {
hook.DenoHook
Client HookDenoClient
Expand All @@ -101,6 +119,15 @@ func (d *SMSDenoHook) Call(ctx context.Context, u *url.URL, payload SendOptions)
return jsonText, nil
}

func (d *SMSDenoHook) Test(ctx context.Context, script string, payload SendOptions) error {
_, err := d.Client.Run(ctx, script, payload)
if err != nil {
return err
}

return nil
}

type CustomClient struct {
Config *config.CustomSMSProviderConfig
SMSDenoHook SMSDenoHook
Expand Down
4 changes: 4 additions & 0 deletions pkg/lib/translation/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func (s *Service) GetSenderForTestEmail(ctx context.Context) (sender string, err
return
}

func (s *Service) GetSenderForTestSMS(ctx context.Context) (sender string, err error) {
return s.smsMessageHeader(ctx, "default", &PreparedTemplateVariables{})
}

func (s *Service) emailMessageHeader(ctx context.Context, name SpecName, variables *PreparedTemplateVariables) (sender, replyTo, subject string, err error) {
t, err := s.translationMap(ctx)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/portal/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/authgear/authgear-server/pkg/portal/libstripe"
"github.com/authgear/authgear-server/pkg/portal/loader"
"github.com/authgear/authgear-server/pkg/portal/service"
"github.com/authgear/authgear-server/pkg/portal/sms"
"github.com/authgear/authgear-server/pkg/portal/smtp"
"github.com/authgear/authgear-server/pkg/portal/transport"
"github.com/authgear/authgear-server/pkg/util/clock"
Expand Down Expand Up @@ -68,6 +69,8 @@ var DependencySet = wire.NewSet(
smtp.DependencySet,
wire.Bind(new(smtp.MailSender), new(*mail.Sender)),

sms.DependencySet,

auditdb.NewReadHandle,
auditdb.NewWriteHandle,
auditdb.DependencySet,
Expand Down Expand Up @@ -110,6 +113,7 @@ var DependencySet = wire.NewSet(
wire.Bind(new(graphql.DomainService), new(*service.DomainService)),
wire.Bind(new(graphql.CollaboratorService), new(*service.CollaboratorService)),
wire.Bind(new(graphql.SMTPService), new(*smtp.Service)),
wire.Bind(new(graphql.SMSService), new(*sms.Service)),
wire.Bind(new(graphql.AppResourceManagerFactory), new(*appresource.ManagerFactory)),
wire.Bind(new(graphql.AnalyticChartService), new(*analytic.ChartService)),
wire.Bind(new(graphql.TutorialService), new(*tutorial.Service)),
Expand Down
13 changes: 13 additions & 0 deletions pkg/portal/graphql/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ type AppService interface {
app *model.App,
returnURI string,
) (*tester.TesterToken, error)
LoadAppWebhookSecretMaterials(
ctx context.Context,
app *model.App) (*config.WebhookKeyMaterials, error)
}

type DomainService interface {
Expand Down Expand Up @@ -98,6 +101,15 @@ type SMTPService interface {
SendTestEmail(ctx context.Context, app *model.App, options smtp.SendTestEmailOptions) (err error)
}

type SMSService interface {
SendTestSMS(
ctx context.Context,
app *model.App,
to string,
webhookSecretLoader func(ctx context.Context) (*config.WebhookKeyMaterials, error),
input model.SMSProviderConfigurationInput) error
}

type AppResourceManagerFactory interface {
NewManagerWithAppContext(appContext *config.AppContext) *appresource.Manager
}
Expand Down Expand Up @@ -188,6 +200,7 @@ type Context struct {
DomainService DomainService
CollaboratorService CollaboratorService
SMTPService SMTPService
SMSService SMSService
AppResMgrFactory AppResourceManagerFactory
AnalyticChartService AnalyticChartService
TutorialService TutorialService
Expand Down
154 changes: 154 additions & 0 deletions pkg/portal/graphql/sms_mutation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package graphql

import (
"context"
"encoding/json"

relay "github.com/authgear/graphql-go-relay"

Check failure on line 7 in pkg/portal/graphql/sms_mutation.go

View workflow job for this annotation

GitHub Actions / checks / authgear-test

no required module provides package github.com/authgear/graphql-go-relay; to add it:

Check failure on line 7 in pkg/portal/graphql/sms_mutation.go

View workflow job for this annotation

GitHub Actions / checks / authgear-test

no required module provides package github.com/authgear/graphql-go-relay; to add it:

Check failure on line 7 in pkg/portal/graphql/sms_mutation.go

View workflow job for this annotation

GitHub Actions / checks / authgear-test

could not import github.com/authgear/graphql-go-relay (invalid package name: "")

Check failure on line 7 in pkg/portal/graphql/sms_mutation.go

View workflow job for this annotation

GitHub Actions / checks / authgear-e2e

no required module provides package github.com/authgear/graphql-go-relay; to add it:
"github.com/graphql-go/graphql"

"github.com/authgear/authgear-server/pkg/api/apierrors"
"github.com/authgear/authgear-server/pkg/lib/config"
"github.com/authgear/authgear-server/pkg/portal/model"
)

var sendTestSMSInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "SendTestSMSInput",
Fields: graphql.InputObjectConfigFieldMap{
"appID": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.ID),
Description: "App ID to test.",
},
"to": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
Description: "The recipient phone number.",
},
"providerConfiguration": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(smsProviderConfigurationInput),
Description: "The SMS provider configuration.",
},
},
})

var smsProviderConfigurationInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "SMSProviderConfigurationInput",
Fields: graphql.InputObjectConfigFieldMap{
"twilio": &graphql.InputObjectFieldConfig{
Type: smsProviderConfigurationTwilioInput,
Description: "Twilio configuration",
},
"webhook": &graphql.InputObjectFieldConfig{
Type: smsProviderConfigurationWebhookInput,
Description: "Webhook Configuration",
},
"deno": &graphql.InputObjectFieldConfig{
Type: smsProviderConfigurationDenoInput,
Description: "Deno hook configuration",
},
},
})

var smsProviderConfigurationTwilioInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "SMSProviderConfigurationTwilioInput",
Fields: graphql.InputObjectConfigFieldMap{
"accountSID": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"authToken": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"messagingServiceSID": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
},
})

var smsProviderConfigurationWebhookInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "SMSProviderConfigurationWebhookInput",
Fields: graphql.InputObjectConfigFieldMap{
"url": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"timeout": &graphql.InputObjectFieldConfig{
Type: graphql.Int,
},
},
})

var smsProviderConfigurationDenoInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "SMSProviderConfigurationDenoInput",
Fields: graphql.InputObjectConfigFieldMap{
"script": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"timeout": &graphql.InputObjectFieldConfig{
Type: graphql.Int,
},
},
})

var _ = registerMutationField(
"sendTestSMSConfiguration",
&graphql.Field{
Description: "Send a SMS to test the configuration",
Type: graphql.Boolean,
Args: graphql.FieldConfigArgument{
"input": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(sendTestSMSInput),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
input := p.Args["input"].(map[string]interface{})
appNodeID := input["appID"].(string)
to := input["to"].(string)
providerConfigurationInput := input["providerConfiguration"]

resolvedNodeID := relay.FromGlobalID(appNodeID)
if resolvedNodeID == nil || resolvedNodeID.Type != typeApp {
return nil, apierrors.NewInvalid("invalid app ID")
}
appID := resolvedNodeID.ID

ctx := p.Context
gqlCtx := GQLContext(ctx)

// Access control: collaborator.
_, err := gqlCtx.AuthzService.CheckAccessOfViewer(ctx, appID)
if err != nil {
return nil, err
}

app, err := gqlCtx.AppService.Get(ctx, appID)
if err != nil {
return nil, err
}

providerConfigJSON, err := json.Marshal(providerConfigurationInput)
if err != nil {
return nil, err
}
var providerConfig model.SMSProviderConfigurationInput
err = json.Unmarshal(providerConfigJSON, &providerConfig)
if err != nil {
return nil, err
}

webhookSecretLoader := func(ctx context.Context) (*config.WebhookKeyMaterials, error) {
return gqlCtx.AppService.LoadAppWebhookSecretMaterials(ctx, app)
}

err = gqlCtx.SMSService.SendTestSMS(
ctx,
app,
to,
webhookSecretLoader,
providerConfig,
)
if err != nil {
return nil, err
}

return nil, nil
},
},
)
23 changes: 23 additions & 0 deletions pkg/portal/model/sms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package model

type SMSProviderConfigurationInput struct {
Twilio *SMSProviderConfigurationTwilioInput `json:"twilio,omitempty"`
Webhook *SMSProviderConfigurationWebhookInput `json:"webhook,omitempty"`
Deno *SMSProviderConfigurationDenoInput `json:"deno,omitempty"`
}

type SMSProviderConfigurationTwilioInput struct {
AccountSID string `json:"accountSID,omitempty"`
AuthToken string `json:"authToken,omitempty"`
MessagingServiceSID *string `json:"messagingServiceSID,omitempty"`
}

type SMSProviderConfigurationWebhookInput struct {
URL string `json:"url,omitempty"`
Timeout *int `json:"timeout,omitempty"`
}

type SMSProviderConfigurationDenoInput struct {
Script string `json:"script,omitempty"`
Timeout *int `json:"timeout,omitempty"`
}
24 changes: 24 additions & 0 deletions pkg/portal/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,27 @@ func (s *AppService) validateAppID(appID string) error {
func (s *AppService) RenderSAMLEntityID(appID string) string {
return saml.RenderSAMLEntityID(s.SAMLEnvironmentConfig, appID)
}

func (s *AppService) LoadAppWebhookSecretMaterials(
ctx context.Context,
app *model.App) (*config.WebhookKeyMaterials, error) {
resMgr := s.AppResMgrFactory.NewManagerWithAppContext(app.Context)
result, err := resMgr.ReadAppFile(configsource.SecretConfig, &resource.AppFile{
Path: configsource.AuthgearSecretYAML,
})
if err != nil {
return nil, nil
}

bytes := result.([]byte)

secretConfig, err := config.ParsePartialSecret(bytes)
if err != nil {
return nil, err
}
if webhook, ok := secretConfig.LookupData(config.WebhookKeyMaterialsKey).(*config.WebhookKeyMaterials); ok {
return webhook, nil
}

return nil, fmt.Errorf("unexpected: app %s is missing webhook secret", app.ID)
}
38 changes: 38 additions & 0 deletions pkg/portal/sms/deps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package sms

import (
"context"

"github.com/google/wire"

"github.com/authgear/authgear-server/pkg/portal/model"
"github.com/authgear/authgear-server/pkg/util/resource"
"github.com/authgear/authgear-server/pkg/util/template"
)

var DependencySet = wire.NewSet(
NewLogger,
wire.Struct(new(Service), "*"),
)

type NoopStaticAssetResolver struct{}

func (r *NoopStaticAssetResolver) StaticAssetURL(ctx context.Context, id string) (url string, err error) {
panic("NoopStaticAssetResolver is not supposed to be reachable")
}

func ProvideStaticAssetResolver() *NoopStaticAssetResolver {
return &NoopStaticAssetResolver{}
}

func ProvideResourceManager(app *model.App) *resource.Manager {
return app.Context.Resources
}

func ProvideDefaultLanguageTag(app *model.App) template.DefaultLanguageTag {
return template.DefaultLanguageTag(*app.Context.Config.AppConfig.Localization.FallbackLanguage)
}

func ProvideSupportedLanguageTags(app *model.App) template.SupportedLanguageTags {
return template.SupportedLanguageTags(app.Context.Config.AppConfig.Localization.SupportedLanguages)
}
Loading

0 comments on commit 775ddcd

Please sign in to comment.