From 1ac060136aac5836d4eb2a94c8dacb6ff160b6e9 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 20 Oct 2022 16:27:00 +0530 Subject: [PATCH 01/11] fix: make env vars name more persistent --- server/constants/env.go | 10 +++++----- server/db/providers/dynamodb/provider.go | 24 +++++++++++++++++------- server/env/env.go | 20 ++++++++++---------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/server/constants/env.go b/server/constants/env.go index cc1ddfc..c0af6a3 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -22,11 +22,11 @@ const ( // EnvKeyDatabaseURL key for env variable DATABASE_URL EnvKeyDatabaseURL = "DATABASE_URL" // EnvAwsRegion key for env variable AWS REGION - EnvAwsRegion = "REGION" - // EnvAwsAccessKey key for env variable AWS_ACCESS_KEY - EnvAwsAccessKey = "AWS_ACCESS_KEY" - // EnvAwsAccessKey key for env variable AWS_SECRET_KEY - EnvAwsSecretKey = "AWS_SECRET_KEY" + EnvAwsRegion = "AWS_REGION" + // EnvAwsAccessKeyID key for env variable AWS_ACCESS_KEY_ID + EnvAwsAccessKeyID = "AWS_ACCESS_KEY_ID" + // EnvAwsAccessKey key for env variable AWS_SECRET_ACCESS_KEY + EnvAwsSecretAccessKey = "AWS_SECRET_ACCESS_KEY" // EnvKeyDatabaseName key for env variable DATABASE_NAME EnvKeyDatabaseName = "DATABASE_NAME" // EnvKeyDatabaseUsername key for env variable DATABASE_USERNAME diff --git a/server/db/providers/dynamodb/provider.go b/server/db/providers/dynamodb/provider.go index 7457613..0caa6ea 100644 --- a/server/db/providers/dynamodb/provider.go +++ b/server/db/providers/dynamodb/provider.go @@ -1,16 +1,18 @@ package dynamodb import ( + "fmt" "os" - "github.com/authorizerdev/authorizer/server/constants" - "github.com/authorizerdev/authorizer/server/db/models" - "github.com/authorizerdev/authorizer/server/memorystore" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/guregu/dynamo" log "github.com/sirupsen/logrus" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db/models" + "github.com/authorizerdev/authorizer/server/memorystore" ) type provider struct { @@ -21,8 +23,8 @@ type provider struct { func NewProvider() (*provider, error) { dbURL := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseURL awsRegion := os.Getenv(constants.EnvAwsRegion) - accessKey := os.Getenv(constants.EnvAwsAccessKey) - secretKey := os.Getenv(constants.EnvAwsSecretKey) + accessKey := os.Getenv(constants.EnvAwsAccessKeyID) + secretKey := os.Getenv(constants.EnvAwsSecretAccessKey) config := aws.Config{ MaxRetries: aws.Int(3), @@ -33,6 +35,16 @@ func NewProvider() (*provider, error) { config.Region = aws.String(awsRegion) } + if accessKey == "" { + log.Debugf("%s not found", constants.EnvAwsAccessKeyID) + return nil, fmt.Errorf("invalid aws credentials. %s not found", constants.EnvAwsAccessKeyID) + } + + if secretKey == "" { + log.Debugf("%s not found", constants.EnvAwsSecretAccessKey) + return nil, fmt.Errorf("invalid aws credentials. %s not found", constants.EnvAwsSecretAccessKey) + } + // custom accessKey, secretkey took first priority, if not then fetch config from aws credentials if accessKey != "" && secretKey != "" { config.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "") @@ -40,8 +52,6 @@ func NewProvider() (*provider, error) { // static config in case of testing or local-setup config.Credentials = credentials.NewStaticCredentials("key", "key", "") config.Endpoint = aws.String(dbURL) - } else { - log.Info("REGION, AWS_ACCESS_KEY and AWS_SECRET_KEY not found in .env, trying to load default profile from aws credentials") } session := session.Must(session.NewSession(&config)) diff --git a/server/env/env.go b/server/env/env.go index 491e4ee..79777f0 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -78,8 +78,8 @@ func InitAllEnv() error { osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) osAwsRegion := os.Getenv(constants.EnvAwsRegion) - osAwsAccessKey := os.Getenv(constants.EnvAwsAccessKey) - osAwsSecretKey := os.Getenv(constants.EnvAwsSecretKey) + osAwsAccessKey := os.Getenv(constants.EnvAwsAccessKeyID) + osAwsSecretKey := os.Getenv(constants.EnvAwsSecretAccessKey) // os bool vars osAppCookieSecure := os.Getenv(constants.EnvKeyAppCookieSecure) @@ -129,18 +129,18 @@ func InitAllEnv() error { envData[constants.EnvAwsRegion] = osAwsRegion } - if val, ok := envData[constants.EnvAwsAccessKey]; !ok || val == "" { - envData[constants.EnvAwsAccessKey] = osAwsAccessKey + if val, ok := envData[constants.EnvAwsAccessKeyID]; !ok || val == "" { + envData[constants.EnvAwsAccessKeyID] = osAwsAccessKey } - if osAwsAccessKey != "" && envData[constants.EnvAwsAccessKey] != osAwsRegion { - envData[constants.EnvAwsAccessKey] = osAwsAccessKey + if osAwsAccessKey != "" && envData[constants.EnvAwsAccessKeyID] != osAwsRegion { + envData[constants.EnvAwsAccessKeyID] = osAwsAccessKey } - if val, ok := envData[constants.EnvAwsSecretKey]; !ok || val == "" { - envData[constants.EnvAwsSecretKey] = osAwsSecretKey + if val, ok := envData[constants.EnvAwsSecretAccessKey]; !ok || val == "" { + envData[constants.EnvAwsSecretAccessKey] = osAwsSecretKey } - if osAwsSecretKey != "" && envData[constants.EnvAwsSecretKey] != osAwsRegion { - envData[constants.EnvAwsSecretKey] = osAwsSecretKey + if osAwsSecretKey != "" && envData[constants.EnvAwsSecretAccessKey] != osAwsRegion { + envData[constants.EnvAwsSecretAccessKey] = osAwsSecretKey } if val, ok := envData[constants.EnvKeyAppURL]; !ok || val == "" { From 094782aeca22e5424c714e91fb532b88c94e4c56 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 21 Oct 2022 11:19:32 +0530 Subject: [PATCH 02/11] fix(server): linting issues --- server/handlers/app.go | 10 +++++----- server/main.go | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/server/handlers/app.go b/server/handlers/app.go index 5b34fb6..6c5d541 100644 --- a/server/handlers/app.go +++ b/server/handlers/app.go @@ -30,7 +30,7 @@ func AppHandler() gin.HandlerFunc { return } - redirect_uri := strings.TrimSpace(c.Query("redirect_uri")) + redirectURI := strings.TrimSpace(c.Query("redirect_uri")) state := strings.TrimSpace(c.Query("state")) scopeString := strings.TrimSpace(c.Query("scope")) @@ -41,11 +41,11 @@ func AppHandler() gin.HandlerFunc { scope = strings.Split(scopeString, " ") } - if redirect_uri == "" { - redirect_uri = hostname + "/app" + if redirectURI == "" { + redirectURI = hostname + "/app" } else { // validate redirect url with allowed origins - if !validators.IsValidOrigin(redirect_uri) { + if !validators.IsValidOrigin(redirectURI) { log.Debug("Invalid redirect_uri") c.JSON(400, gin.H{"error": "invalid redirect url"}) return @@ -75,7 +75,7 @@ func AppHandler() gin.HandlerFunc { c.HTML(http.StatusOK, "app.tmpl", gin.H{ "data": map[string]interface{}{ "authorizerURL": hostname, - "redirectURL": redirect_uri, + "redirectURL": redirectURI, "scope": scope, "state": state, "organizationName": orgName, diff --git a/server/main.go b/server/main.go index f0b544a..fa397c8 100644 --- a/server/main.go +++ b/server/main.go @@ -15,12 +15,15 @@ import ( "github.com/authorizerdev/authorizer/server/routes" ) +// VERSION is used to define the version of authorizer from build tags var VERSION string +// LogUTCFormatter hels in setting UTC time format for the logs type LogUTCFormatter struct { log.Formatter } +// Format helps fomratting time to UTC func (u LogUTCFormatter) Format(e *log.Entry) ([]byte, error) { e.Time = e.Time.UTC() return u.Formatter.Format(e) From 476bdf00fc942b08338f311db6b4ca821afad3b6 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 21 Oct 2022 11:21:21 +0530 Subject: [PATCH 03/11] fix(server): open_id config --- server/handlers/openid_config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/handlers/openid_config.go b/server/handlers/openid_config.go index 781caf1..33ad090 100644 --- a/server/handlers/openid_config.go +++ b/server/handlers/openid_config.go @@ -17,14 +17,14 @@ func OpenIDConfigurationHandler() gin.HandlerFunc { c.JSON(200, gin.H{ "issuer": issuer, "authorization_endpoint": issuer + "/authorize", - "token_endpoint": issuer + "/token", + "token_endpoint": issuer + "/oauth/token", "userinfo_endpoint": issuer + "/userinfo", "jwks_uri": issuer + "/.well-known/jwks.json", - "response_types_supported": []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token"}, + "response_types_supported": []string{"code", "token", "id_token"}, "scopes_supported": []string{"openid", "email", "profile", "email_verified", "given_name", "family_name", "nick_name", "picture"}, - "response_modes_supported": []string{"query", "fragment", "form_post"}, + "response_modes_supported": []string{"query", "fragment", "form_post", "web_message"}, "id_token_signing_alg_values_supported": []string{jwtType}, - "claims_supported": []string{"aud", "exp", "iss", "iat", "sub", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "picture", "email", "email_verified", "roles", "gender", "birthdate", "phone_number", "phone_number_verified"}, + "claims_supported": []string{"aud", "exp", "iss", "iat", "sub", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "picture", "email", "email_verified", "roles", "role", "gender", "birthdate", "phone_number", "phone_number_verified", "nonce", "updated_at", "created_at", "revoked_timestamp", "login_method", "signup_methods", "token_type"}, }) } } From 8449821d1bf0f1e40fe1678bf4d4ce563ef3aab9 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 21 Oct 2022 15:55:54 +0530 Subject: [PATCH 04/11] fix(server): dynamodb tests + provider config --- .env.test | 3 ++- server/db/providers/dynamodb/provider.go | 28 ++++++++---------------- server/db/providers/dynamodb/user.go | 2 +- server/memorystore/memory_store.go | 2 +- server/memorystore/required_env_store.go | 17 +++++++++++--- server/test/resolvers_test.go | 14 ++++++++++-- server/test/test.go | 2 +- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/.env.test b/.env.test index 0df0238..7db07e8 100644 --- a/.env.test +++ b/.env.test @@ -6,4 +6,5 @@ SMTP_HOST=smtp.mailtrap.io SMTP_PORT=2525 SMTP_USERNAME=test SMTP_PASSWORD=test -SENDER_EMAIL="info@authorizer.dev" \ No newline at end of file +SENDER_EMAIL="info@authorizer.dev" +AWS_REGION=ap-south-1 \ No newline at end of file diff --git a/server/db/providers/dynamodb/provider.go b/server/db/providers/dynamodb/provider.go index 0caa6ea..81aa8ce 100644 --- a/server/db/providers/dynamodb/provider.go +++ b/server/db/providers/dynamodb/provider.go @@ -1,9 +1,6 @@ package dynamodb import ( - "fmt" - "os" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" @@ -22,36 +19,29 @@ type provider struct { // NewProvider returns a new Dynamo provider func NewProvider() (*provider, error) { dbURL := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseURL - awsRegion := os.Getenv(constants.EnvAwsRegion) - accessKey := os.Getenv(constants.EnvAwsAccessKeyID) - secretKey := os.Getenv(constants.EnvAwsSecretAccessKey) + awsRegion := memorystore.RequiredEnvStoreObj.GetRequiredEnv().AwsRegion + awsAccessKeyID := memorystore.RequiredEnvStoreObj.GetRequiredEnv().AwsAccessKeyID + awsSecretAccessKey := memorystore.RequiredEnvStoreObj.GetRequiredEnv().AwsSecretAccessKey config := aws.Config{ MaxRetries: aws.Int(3), CredentialsChainVerboseErrors: aws.Bool(true), // for full error logs + } if awsRegion != "" { config.Region = aws.String(awsRegion) } - if accessKey == "" { - log.Debugf("%s not found", constants.EnvAwsAccessKeyID) - return nil, fmt.Errorf("invalid aws credentials. %s not found", constants.EnvAwsAccessKeyID) - } - - if secretKey == "" { - log.Debugf("%s not found", constants.EnvAwsSecretAccessKey) - return nil, fmt.Errorf("invalid aws credentials. %s not found", constants.EnvAwsSecretAccessKey) - } - - // custom accessKey, secretkey took first priority, if not then fetch config from aws credentials - if accessKey != "" && secretKey != "" { - config.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "") + // custom awsAccessKeyID, awsSecretAccessKey took first priority, if not then fetch config from aws credentials + if awsAccessKeyID != "" && awsSecretAccessKey != "" { + config.Credentials = credentials.NewStaticCredentials(awsAccessKeyID, awsSecretAccessKey, "") } else if dbURL != "" { // static config in case of testing or local-setup config.Credentials = credentials.NewStaticCredentials("key", "key", "") config.Endpoint = aws.String(dbURL) + } else { + log.Debugf("%s or %s or %s not found. Trying to load default credentials from aws config", constants.EnvAwsRegion, constants.EnvAwsAccessKeyID, constants.EnvAwsSecretAccessKey) } session := session.Must(session.NewSession(&config)) diff --git a/server/db/providers/dynamodb/user.go b/server/db/providers/dynamodb/user.go index ef780b1..3bd45db 100644 --- a/server/db/providers/dynamodb/user.go +++ b/server/db/providers/dynamodb/user.go @@ -180,7 +180,7 @@ func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, for _, user := range allUsers { err = UpdateByHashKey(userCollection, "id", user.ID, data) - if err != nil { + if err == nil { res = res + 1 } } diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index c112c01..15b7248 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -57,7 +57,7 @@ func InitMemStore() error { } redisURL := requiredEnvs.RedisURL - if redisURL != "" && !requiredEnvs.disableRedisForEnv { + if redisURL != "" && !requiredEnvs.DisableRedisForEnv { log.Info("Initializing Redis memory store") Provider, err = redis.NewRedisProvider(redisURL) if err != nil { diff --git a/server/memorystore/required_env_store.go b/server/memorystore/required_env_store.go index 13166ac..9eafe24 100644 --- a/server/memorystore/required_env_store.go +++ b/server/memorystore/required_env_store.go @@ -27,7 +27,11 @@ type RequiredEnv struct { DatabaseCertKey string `json:"DATABASE_CERT_KEY"` DatabaseCACert string `json:"DATABASE_CA_CERT"` RedisURL string `json:"REDIS_URL"` - disableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` + DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` + // AWS Related Envs + AwsRegion string `json:"AWS_REGION"` + AwsAccessKeyID string `json:"AWS_ACCESS_KEY_ID"` + AwsSecretAccessKey string `json:"AWS_SECRET_ACCESS_KEY"` } // RequiredEnvObj is a simple in-memory store for sessions. @@ -53,7 +57,8 @@ func (r *RequiredEnvStore) SetRequiredEnv(requiredEnv RequiredEnv) { var RequiredEnvStoreObj *RequiredEnvStore -// InitRequiredEnv to initialize EnvData and through error if required env are not present +// InitRequiredEnv to initialize EnvData and throw error if required env are not present +// This includes env that only configurable via env vars and not the ui func InitRequiredEnv() error { envPath := os.Getenv(constants.EnvKeyEnvPath) @@ -85,6 +90,9 @@ func InitRequiredEnv() error { dbCACert := os.Getenv(constants.EnvKeyDatabaseCACert) redisURL := os.Getenv(constants.EnvKeyRedisURL) disableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv) == "true" + awsRegion := os.Getenv(constants.EnvAwsRegion) + awsAccessKeyID := os.Getenv(constants.EnvAwsAccessKeyID) + awsSecretAccessKey := os.Getenv(constants.EnvAwsSecretAccessKey) if strings.TrimSpace(redisURL) == "" { if cli.ARG_REDIS_URL != nil && *cli.ARG_REDIS_URL != "" { @@ -139,7 +147,10 @@ func InitRequiredEnv() error { DatabaseCertKey: dbCertKey, DatabaseCACert: dbCACert, RedisURL: redisURL, - disableRedisForEnv: disableRedisForEnv, + DisableRedisForEnv: disableRedisForEnv, + AwsRegion: awsRegion, + AwsAccessKeyID: awsAccessKeyID, + AwsSecretAccessKey: awsSecretAccessKey, } RequiredEnvStoreObj = &RequiredEnvStore{ diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index 1fc6060..0ff918b 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -2,6 +2,7 @@ package test import ( "context" + "fmt" "os" "strings" "testing" @@ -20,7 +21,7 @@ func TestResolvers(t *testing.T) { constants.DbTypeArangodb: "http://localhost:8529", constants.DbTypeMongodb: "mongodb://localhost:27017", constants.DbTypeScyllaDB: "127.0.0.1:9042", - constants.DbTypeDynamoDB: "http://127.0.0.1:8000", + constants.DbTypeDynamoDB: "http://0.0.0.0:8000", } testDBs := strings.Split(os.Getenv("TEST_DBS"), ",") @@ -52,6 +53,12 @@ func TestResolvers(t *testing.T) { os.Setenv(constants.EnvKeyDatabaseURL, dbURL) os.Setenv(constants.EnvKeyDatabaseType, dbType) os.Setenv(constants.EnvKeyDatabaseName, testDb) + + if dbType == constants.DbTypeDynamoDB { + memorystore.Provider.UpdateEnvVariable(constants.EnvAwsRegion, "ap-south-1") + os.Setenv(constants.EnvAwsRegion, "ap-south-1") + } + memorystore.InitRequiredEnv() err := db.InitDB() @@ -61,12 +68,15 @@ func TestResolvers(t *testing.T) { // clean the persisted config for test to use fresh config envData, err := db.Provider.GetEnv(ctx) - if err == nil { + fmt.Println("envData", envData.ID, envData.EnvData) + if err == nil && envData.ID != "" { envData.EnvData = "" _, err = db.Provider.UpdateEnv(ctx, envData) if err != nil { t.Errorf("Error updating env: %s", err.Error()) } + } else if err != nil { + t.Errorf("Error getting env: %s", err.Error()) } err = env.PersistEnv() if err != nil { diff --git a/server/test/test.go b/server/test/test.go index 1e5105b..1fb6492 100644 --- a/server/test/test.go +++ b/server/test/test.go @@ -84,7 +84,7 @@ func testSetup() TestSetup { testData := TestData{ Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()), Password: "Test@123", - WebhookEndpoint: "https://62cbc6738042b16aa7c22df2.mockapi.io/api/v1/webhook", + WebhookEndpoint: "https://62f93101e05644803533cf36.mockapi.io/authorizer/webhook", TestWebhookEventTypes: []string{constants.UserAccessEnabledWebhookEvent, constants.UserAccessRevokedWebhookEvent, constants.UserCreatedWebhookEvent, constants.UserDeletedWebhookEvent, constants.UserLoginWebhookEvent, constants.UserSignUpWebhookEvent}, TestEmailTemplateEventTypes: []string{constants.VerificationTypeBasicAuthSignup, constants.VerificationTypeForgotPassword, constants.VerificationTypeMagicLinkLogin, constants.VerificationTypeUpdateEmail}, } From 7fc69dcc5523347d6ff3e2b82a2bd386e07c5816 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 21 Oct 2022 21:58:56 +0530 Subject: [PATCH 05/11] fix(server): sql server not allow multiple null multiple null values for unique constrained column is not allowed on sqlserver Resolves #278 --- server/db/providers/sql/provider.go | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/server/db/providers/sql/provider.go b/server/db/providers/sql/provider.go index 712f3d1..cfe83eb 100644 --- a/server/db/providers/sql/provider.go +++ b/server/db/providers/sql/provider.go @@ -1,6 +1,7 @@ package sql import ( + "fmt" "log" "os" "time" @@ -21,6 +22,16 @@ type provider struct { db *gorm.DB } +const ( + phoneNumberIndexName = "UQ_phone_number" + phoneNumberColumnName = "phone_number" +) + +type indexInfo struct { + IndexName string `json:"index_name"` + ColumnName string `json:"column_name"` +} + // NewProvider returns a new SQL provider func NewProvider() (*provider, error) { var sqlDB *gorm.DB @@ -65,6 +76,32 @@ func NewProvider() (*provider, error) { if err != nil { return nil, err } + + // unique constraint on phone number does not work with multiple null values for sqlserver + // for more information check https://stackoverflow.com/a/767702 + if dbType == constants.DbTypeSqlserver { + var indexInfos []indexInfo + // remove index on phone number if present with different name + res := sqlDB.Raw("SELECT i.name AS index_name, i.type_desc AS index_algorithm, CASE i.is_unique WHEN 1 THEN 'TRUE' ELSE 'FALSE' END AS is_unique, ac.Name AS column_name FROM sys.tables AS t INNER JOIN sys.indexes AS i ON t.object_id = i.object_id INNER JOIN sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id INNER JOIN sys.all_columns AS ac ON ic.object_id = ac.object_id AND ic.column_id = ac.column_id WHERE t.name = 'authorizer_users' AND SCHEMA_NAME(t.schema_id) = 'dbo';").Scan(&indexInfos) + if res.Error != nil { + return nil, res.Error + } + + for _, val := range indexInfos { + if val.ColumnName == phoneNumberColumnName && val.IndexName != phoneNumberIndexName { + // drop index & create new + if res := sqlDB.Exec(fmt.Sprintf(`ALTER TABLE authorizer_users DROP CONSTRAINT "%s";`, val.IndexName)); res.Error != nil { + return nil, res.Error + } + + // create index + if res := sqlDB.Exec(fmt.Sprintf("CREATE UNIQUE NONCLUSTERED INDEX %s ON authorizer_users(phone_number) WHERE phone_number IS NOT NULL;", phoneNumberIndexName)); res.Error != nil { + return nil, res.Error + } + } + } + } + return &provider{ db: sqlDB, }, nil From 0f67d746571098735e3f84e1a35bf099245774a9 Mon Sep 17 00:00:00 2001 From: anik-ghosh-au7 Date: Sun, 23 Oct 2022 22:59:17 +0530 Subject: [PATCH 06/11] feat: add user roles multi select input --- dashboard/src/components/EditUserModal.tsx | 467 ++++++------- dashboard/src/components/InputField.tsx | 746 ++++++++++++--------- dashboard/src/constants.ts | 485 +++++++------- dashboard/src/graphql/queries/index.ts | 9 + 4 files changed, 912 insertions(+), 795 deletions(-) diff --git a/dashboard/src/components/EditUserModal.tsx b/dashboard/src/components/EditUserModal.tsx index 265a10c..e1a3e0f 100644 --- a/dashboard/src/components/EditUserModal.tsx +++ b/dashboard/src/components/EditUserModal.tsx @@ -1,250 +1,263 @@ -import React from 'react'; +import React, { useState } from 'react'; import { - Button, - Center, - Flex, - MenuItem, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Stack, - useDisclosure, - Text, - useToast, + Button, + Center, + Flex, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + useDisclosure, + Text, + useToast, } from '@chakra-ui/react'; import { useClient } from 'urql'; import { FaSave } from 'react-icons/fa'; import InputField from './InputField'; import { - ArrayInputType, - DateInputType, - SelectInputType, - TextInputType, + DateInputType, + MultiSelectInputType, + SelectInputType, + TextInputType, } from '../constants'; import { getObjectDiff } from '../utils'; import { UpdateUser } from '../graphql/mutation'; +import { GetAvailableRolesQuery } from '../graphql/queries'; const GenderTypes = { - Undisclosed: null, - Male: 'Male', - Female: 'Female', + Undisclosed: null, + Male: 'Male', + Female: 'Female', }; interface userDataTypes { - id: string; - email: string; - given_name: string; - family_name: string; - middle_name: string; - nickname: string; - gender: string; - birthdate: string; - phone_number: string; - picture: string; - roles: [string] | []; + id: string; + email: string; + given_name: string; + family_name: string; + middle_name: string; + nickname: string; + gender: string; + birthdate: string; + phone_number: string; + picture: string; + roles: [string] | []; } const EditUserModal = ({ - user, - updateUserList, + user, + updateUserList, }: { - user: userDataTypes; - updateUserList: Function; + user: userDataTypes; + updateUserList: Function; }) => { - const client = useClient(); - const toast = useToast(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const [userData, setUserData] = React.useState({ - id: '', - email: '', - given_name: '', - family_name: '', - middle_name: '', - nickname: '', - gender: '', - birthdate: '', - phone_number: '', - picture: '', - roles: [], - }); - React.useEffect(() => { - setUserData(user); - }, []); - const saveHandler = async () => { - const diff = getObjectDiff(user, userData); - const updatedUserData = diff.reduce( - (acc: any, property: string) => ({ - ...acc, - // @ts-ignore - [property]: userData[property], - }), - {}, - ); - const res = await client - .mutation(UpdateUser, { params: { ...updatedUserData, id: userData.id } }) - .toPromise(); - if (res.error) { - toast({ - title: 'User data update failed', - isClosable: true, - status: 'error', - position: 'bottom-right', - }); - } else if (res.data?._update_user?.id) { - toast({ - title: 'User data update successful', - isClosable: true, - status: 'success', - position: 'bottom-right', - }); - } - onClose(); - updateUserList(); - }; - return ( - <> - Edit User Details - - - - Edit User Details - - - - - - Given Name: - -
- -
-
- - - Middle Name: - -
- -
-
- - - Family Name: - -
- -
-
- - - Birth Date: - -
- -
-
- - - Nickname: - -
- -
-
- - - Gender: - -
- -
-
- - - Phone Number: - -
- -
-
- - - Picture: - -
- -
-
- - - Roles: - -
- -
-
-
-
+ const client = useClient(); + const toast = useToast(); + const [availableRoles, setAvailableRoles] = useState([]); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [userData, setUserData] = useState({ + id: '', + email: '', + given_name: '', + family_name: '', + middle_name: '', + nickname: '', + gender: '', + birthdate: '', + phone_number: '', + picture: '', + roles: [], + }); + React.useEffect(() => { + setUserData(user); + fetchAvailableRoles(); + }, []); + const fetchAvailableRoles = async () => { + const res = await client.query(GetAvailableRolesQuery).toPromise(); + if (res.data?._env?.ROLES && res.data?._env?.PROTECTED_ROLES) { + setAvailableRoles([ + ...res.data._env.ROLES, + ...res.data._env.PROTECTED_ROLES, + ]); + } + }; + const saveHandler = async () => { + const diff = getObjectDiff(user, userData); + const updatedUserData = diff.reduce( + (acc: any, property: string) => ({ + ...acc, + // @ts-ignore + [property]: userData[property], + }), + {}, + ); + const res = await client + .mutation(UpdateUser, { params: { ...updatedUserData, id: userData.id } }) + .toPromise(); + if (res.error) { + toast({ + title: 'User data update failed', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else if (res.data?._update_user?.id) { + toast({ + title: 'User data update successful', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + onClose(); + updateUserList(); + }; + return ( + <> + Edit User Details + + + + Edit User Details + + + + + + Given Name: + +
+ +
+
+ + + Middle Name: + +
+ +
+
+ + + Family Name: + +
+ +
+
+ + + Birth Date: + +
+ +
+
+ + + Nickname: + +
+ +
+
+ + + Gender: + +
+ +
+
+ + + Phone Number: + +
+ +
+
+ + + Picture: + +
+ +
+
+ + + Roles: + +
+ +
+
+
+
- - - -
-
- - ); + + + +
+
+ + ); }; export default EditUserModal; diff --git a/dashboard/src/components/InputField.tsx b/dashboard/src/components/InputField.tsx index 651f54b..fe1d3d6 100644 --- a/dashboard/src/components/InputField.tsx +++ b/dashboard/src/components/InputField.tsx @@ -1,340 +1,432 @@ -import React from 'react'; +import React, { useState } from 'react'; import { - Box, - Flex, - Input, - Center, - InputGroup, - InputRightElement, - Tag, - TagLabel, - TagRightIcon, - Select, - Textarea, - Switch, - Text, + Box, + Flex, + Input, + Center, + InputGroup, + InputRightElement, + Tag, + TagLabel, + TagRightIcon, + Select, + Textarea, + Switch, + Text, + MenuButton, + MenuList, + MenuItemOption, + MenuOptionGroup, + Button, + Menu, } from '@chakra-ui/react'; import { - FaRegClone, - FaRegEye, - FaRegEyeSlash, - FaPlus, - FaTimes, + FaRegClone, + FaRegEye, + FaRegEyeSlash, + FaPlus, + FaTimes, + FaAngleDown, } from 'react-icons/fa'; import { - ArrayInputOperations, - ArrayInputType, - SelectInputType, - HiddenInputType, - TextInputType, - TextAreaInputType, - SwitchInputType, - DateInputType, + ArrayInputOperations, + ArrayInputType, + SelectInputType, + HiddenInputType, + TextInputType, + TextAreaInputType, + SwitchInputType, + DateInputType, + MultiSelectInputType, } from '../constants'; import { copyTextToClipboard } from '../utils'; const InputField = ({ - inputType, - variables, - setVariables, - fieldVisibility, - setFieldVisibility, - ...downshiftProps + inputType, + variables, + setVariables, + fieldVisibility, + setFieldVisibility, + availableRoles, + ...downshiftProps }: any) => { - const props = { - size: 'sm', - ...downshiftProps, - }; - const [inputFieldVisibility, setInputFieldVisibility] = React.useState< - Record - >({ - ROLES: false, - DEFAULT_ROLES: false, - PROTECTED_ROLES: false, - ALLOWED_ORIGINS: false, - roles: false, - }); - const [inputData, setInputData] = React.useState>({ - ROLES: '', - DEFAULT_ROLES: '', - PROTECTED_ROLES: '', - ALLOWED_ORIGINS: '', - roles: '', - }); - const updateInputHandler = ( - type: string, - operation: any, - role: string = '', - ) => { - if (operation === ArrayInputOperations.APPEND) { - if (inputData[type] !== '') { - setVariables({ - ...variables, - [type]: [...variables[type], inputData[type]], - }); - setInputData({ ...inputData, [type]: '' }); - } - setInputFieldVisibility({ ...inputFieldVisibility, [type]: false }); - } - if (operation === ArrayInputOperations.REMOVE) { - let updatedEnvVars = variables[type].filter( - (item: string) => item !== role, - ); - setVariables({ - ...variables, - [type]: updatedEnvVars, - }); - } - }; - if (Object.values(TextInputType).includes(inputType)) { - return ( - - - setVariables({ - ...variables, - [inputType]: event.target.value, - }) - } - /> - } - cursor="pointer" - onClick={() => copyTextToClipboard(variables[inputType])} - /> - - ); - } - if (Object.values(HiddenInputType).includes(inputType)) { - return ( - - - setVariables({ - ...variables, - [inputType]: event.target.value, - }) - } - type={!fieldVisibility[inputType] ? 'password' : 'text'} - /> - - {fieldVisibility[inputType] ? ( -
- setFieldVisibility({ - ...fieldVisibility, - [inputType]: false, - }) - } - > - -
- ) : ( -
- setFieldVisibility({ - ...fieldVisibility, - [inputType]: true, - }) - } - > - -
- )} -
copyTextToClipboard(variables[inputType])} - > - -
- - } - /> -
- ); - } - if (Object.values(ArrayInputType).includes(inputType)) { - return ( - 3 ? 'scroll' : 'hidden'} - overflowY="hidden" - justifyContent="start" - alignItems="center" - > - {variables[inputType].map((role: string, index: number) => ( - - - {role} - - updateInputHandler( - inputType, - ArrayInputOperations.REMOVE, - role, - ) - } - /> - - - ))} - {inputFieldVisibility[inputType] ? ( - - { - setInputData({ ...inputData, [inputType]: e.target.value }); - }} - onBlur={() => - updateInputHandler(inputType, ArrayInputOperations.APPEND) - } - onKeyPress={(event) => { - if (event.key === 'Enter') { - updateInputHandler(inputType, ArrayInputOperations.APPEND); - } - }} - /> - - ) : ( - - setInputFieldVisibility({ - ...inputFieldVisibility, - [inputType]: true, - }) - } - > - - - - - )} - - ); - } - if (Object.values(SelectInputType).includes(inputType)) { - const { options, ...rest } = props; - return ( - - ); - } - if (Object.values(TextAreaInputType).includes(inputType)) { - return ( -