feat: merge with mailgun
All checks were successful
deploy / deploy (push) Successful in 1m32s

This commit is contained in:
Stepan Vladovskiy 2024-04-11 17:14:25 -03:00
commit 8eb9650142
15 changed files with 266 additions and 26 deletions

View File

@ -33,3 +33,4 @@ jobs:
branch: 'main'
git_remote_url: 'ssh://dokku@staging.discours.io:22/authorizer'
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
git_push_flags: '--force'

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module server
go 1.21.5
require (
github.com/99designs/gqlgen v0.17.43 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.1.0 // indirect
github.com/urfave/cli/v2 v2.25.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.11 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.9.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

31
go.sum Normal file
View File

@ -0,0 +1,31 @@
github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuFCPo=
github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4=
github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/urfave/cli/v2 v2.25.5 h1:d0NIAyhh5shGscroL7ek/Ya9QYQE0KNabJgiUinIQkc=
github.com/urfave/cli/v2 v2.25.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -19,6 +19,7 @@ require (
github.com/google/uuid v1.3.1
github.com/guregu/dynamo v1.20.2
github.com/joho/godotenv v1.5.1
github.com/mailgun/mailgun-go/v4 v4.12.0
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.2.1
github.com/robertkrimen/otto v0.2.1
@ -83,7 +84,6 @@ require (
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libsql/libsql-client-go v0.0.0-20231026052543-fce76c0f39a7 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect
github.com/mailgun/mailgun-go/v4 v4.12.0 // indirect
github.com/maruel/rs v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/microsoft/go-mssqldb v1.6.0 // indirect

View File

@ -81,8 +81,11 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ekristen/gorm-libsql v0.0.0-20231101204708-6e113112bcc2 h1:3f6DAUkYKbZSJ1bBM0/RiX5NHVt7YgmB0BWzKWUd45g=
github.com/ekristen/gorm-libsql v0.0.0-20231101204708-6e113112bcc2/go.mod h1:5g9wSYpR/MvkR6W7SumX9zdha7Yt1iM4nxOAWfRfcPA=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=

View File

@ -236,6 +236,7 @@ type ComplexityRoot struct {
AdminSession func(childComplexity int) int
EmailTemplates func(childComplexity int, params *model.PaginatedInput) int
Env func(childComplexity int) int
IsRegistered func(childComplexity int, email string) int
Meta func(childComplexity int) int
Profile func(childComplexity int) int
Session func(childComplexity int, params *model.SessionQueryInput) int
@ -392,6 +393,7 @@ type QueryResolver interface {
Meta(ctx context.Context) (*model.Meta, error)
Session(ctx context.Context, params *model.SessionQueryInput) (*model.AuthResponse, error)
Profile(ctx context.Context) (*model.User, error)
IsRegistered(ctx context.Context, email string) (*model.Response, error)
ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error)
ValidateSession(ctx context.Context, params *model.ValidateSessionInput) (*model.ValidateSessionResponse, error)
Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error)
@ -1666,6 +1668,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Env(childComplexity), true
case "Query.is_registered":
if e.complexity.Query.IsRegistered == nil {
break
}
args, err := ec.field_Query_is_registered_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.IsRegistered(childComplexity, args["email"].(string)), true
case "Query.meta":
if e.complexity.Query.Meta == nil {
break
@ -3036,6 +3050,7 @@ type Query {
meta: Meta!
session(params: SessionQueryInput): AuthResponse!
profile: User!
is_registered(email: String!): Response! # custom api
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
validate_session(params: ValidateSessionInput): ValidateSessionResponse!
# admin only apis
@ -3612,6 +3627,21 @@ func (ec *executionContext) field_Query__webhooks_args(ctx context.Context, rawA
return args, nil
}
func (ec *executionContext) field_Query_is_registered_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 string
if tmp, ok := rawArgs["email"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
arg0, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["email"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_session_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -11208,6 +11238,65 @@ func (ec *executionContext) fieldContext_Query_profile(ctx context.Context, fiel
return fc, nil
}
func (ec *executionContext) _Query_is_registered(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_is_registered(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().IsRegistered(rctx, fc.Args["email"].(string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_is_registered(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "message":
return ec.fieldContext_Response_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Response", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_is_registered_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Query_validate_jwt_token(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_validate_jwt_token(ctx, field)
if err != nil {
@ -20511,6 +20600,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "is_registered":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_is_registered(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx,
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "validate_jwt_token":
field := field

View File

@ -610,7 +610,6 @@ input GetUserRequest {
}
type Mutation {
is_registered(email: String): AuthResponse! # custom api
signup(params: SignUpInput!): AuthResponse!
# Deprecated from v1.2.0
mobile_signup(params: MobileSignUpInput): AuthResponse!
@ -652,6 +651,7 @@ type Query {
meta: Meta!
session(params: SessionQueryInput): AuthResponse!
profile: User!
is_registered(email: String!): Response! # custom api
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
validate_session(params: ValidateSessionInput): ValidateSessionResponse!
# admin only apis

View File

@ -12,11 +12,6 @@ import (
"github.com/authorizerdev/authorizer/server/resolvers"
)
// Signup is the resolver for the signup field.
func (r *queryResolver) IsRegistered(ctx context.Context, email string) (*model.AuthResponse, error) {
return resolvers.IsRegisteredResolver(ctx, email)
}
// Signup is the resolver for the signup field.
func (r *mutationResolver) Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, error) {
return resolvers.SignupResolver(ctx, params)
@ -192,6 +187,11 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
return resolvers.ProfileResolver(ctx)
}
// IsRegistered is the resolver for the signup field.
func (r *queryResolver) IsRegistered(ctx context.Context, email string) (*model.Response, error) {
return resolvers.IsRegisteredResolver(ctx, email)
}
// ValidateJwtToken is the resolver for the validate_jwt_token field.
func (r *queryResolver) ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) {
return resolvers.ValidateJwtTokenResolver(ctx, params)

View File

@ -3,8 +3,11 @@ package inmemory
import (
"fmt"
"os"
"errors"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore/providers/redis"
)
// SetUserSession sets the user session for given user identifier in form recipe:user_id
@ -119,3 +122,25 @@ func (c *provider) GetBoolStoreEnvVariable(key string) (bool, error) {
}
return res.(bool), nil
}
// GetUserAppDataFromRedis retrieves user profile and follows from Redis, combines them into a JSON format,
// and assigns the JSON string to the provided user's ID.
func (c *provider) GetUserAppDataFromRedis(userId string) (string, error) {
redisURL := os.Getenv(constants.EnvKeyRedisURL)
if redisURL == "" {
return "", errors.New("Redis URL not found")
}
log.Info("Initializing Redis provider")
red, err := redis.NewRedisProvider(redisURL)
if err != nil {
return "", fmt.Errorf("failed to initialize Redis provider: %w", err)
}
combinedData, err := red.GetUserAppDataFromRedis(userId)
if err != nil {
return "", fmt.Errorf("failed to get Redis app data: %w", err)
}
return combinedData, nil
}

View File

@ -38,4 +38,6 @@ type Provider interface {
GetStringStoreEnvVariable(key string) (string, error)
// GetBoolStoreEnvVariable to get the bool env variable from env store
GetBoolStoreEnvVariable(key string) (bool, error)
GetUserAppDataFromRedis(userId string) (string, error)
}

View File

@ -218,3 +218,16 @@ func (c *provider) GetBoolStoreEnvVariable(key string) (bool, error) {
return data == "1", nil
}
// GetUserAppDataFromRedis retrieves user profile and follows from Redis, combines them into a JSON format,
// and assigns the JSON string to the provided user's ID.
func (c *provider) GetUserAppDataFromRedis(userId string) (string, error) {
// Retrieve user data from Redis
userProfile := c.store.Get(c.ctx, fmt.Sprintf(`user:%s:author`, userId))
userFollows := c.store.Get(c.ctx, fmt.Sprintf(`user:%s:follows`, userId))
// Combine user data into a JSON string
combinedData := fmt.Sprintf(`{"profile": %s, "follows": %s}`, userProfile, userFollows)
return combinedData, nil
}

View File

@ -83,6 +83,16 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
log.Debug("Failed to get user: ", err)
return nil, fmt.Errorf(`bad user credentials`)
}
if user.SignupMethods == "magic_link_login" {
user.SignupMethods = "basic_auth"
user, err = db.Provider.UpdateUser(ctx, user)
if err != nil {
log.Debug("Failed to update user signup method: ", err)
}
}
hostname := parsers.GetHost(gc)
_, nonceHash, err := utils.GenerateNonce()
if err != nil {

View File

@ -11,32 +11,43 @@ import (
)
// IsRegisteredResolver is a resolver for registered checkup query
func IsRegisteredResolver(ctx context.Context, email string) (*model.AuthResponse, error) {
var res *model.AuthResponse
email = strings.TrimSpace(refs.StringValue(&email))
func IsRegisteredResolver(ctx context.Context, email string) (*model.Response, error) {
// Initialize the response object
res := &model.Response{}
res.Message = ""
// Convert email to lowercase
email = strings.ToLower(strings.TrimSpace(refs.StringValue(&email)))
if email == "" {
log.Debug("Email is required")
return res, fmt.Errorf(`email is required`)
return res, fmt.Errorf("email is required")
}
// Initialize logger with a field
log := log.WithField("email", email)
// find user with email
// Find user with email
existingUser, err := db.Provider.GetUserByEmail(ctx, email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
}
if existingUser != nil {
res.Message = "registered"
if existingUser.EmailVerifiedAt != nil {
res.Message = "verified"
log.Debug("Email is already verified and signed up.")
return res, fmt.Errorf(`%s has already signed up`, email)
} else if existingUser.ID != "" && existingUser.EmailVerifiedAt == nil {
res.Message = "not verified"
log.Debug("Email is already signed up. Verification pending...")
return res, fmt.Errorf("%s has already signed up. please complete the email verification process or reset the password", email)
} else {
log.Debug("Found user by email: ", existingUser)
if existingUser != nil {
if existingUser.SignupMethods == "magic_link_login" {
res.Message = "registered"
} else if existingUser.EmailVerifiedAt != nil {
res.Message = "verified"
log.Debug("Email is already verified and signed up.")
return res, nil
} else if existingUser.ID != "" && existingUser.EmailVerifiedAt == nil {
res.Message = "not verified"
log.Debug("Email is already signed up. Verification pending...")
return res, nil
} else {
res.Message = "unknown"
log.Debug("Unknown signup method.")
return res, nil
}
}
}

View File

@ -316,6 +316,12 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
expiresIn = 1
}
appData, err := memorystore.Provider.GetUserAppDataFromRedis(user.ID)
if err == nil {
// Assign the combined data to the provided pointer
user.AppData = &appData
}
res = &model.AuthResponse{
Message: `Logged in successfully`,
AccessToken: &authToken.AccessToken.Token,

View File

@ -90,6 +90,12 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
expiresIn = 1
}
appData, err := memorystore.Provider.GetUserAppDataFromRedis(user.ID)
if err == nil {
// Assign the combined data to the provided pointer
user.AppData = &appData
}
res = &model.AuthResponse{
Message: `Session token refreshed`,
AccessToken: &authToken.AccessToken.Token,
@ -102,6 +108,7 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash, authToken.SessionTokenExpiresAt)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token, authToken.AccessToken.ExpiresAt)
if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token, authToken.RefreshToken.ExpiresAt)