From ab01ff249d06c7490d408047880be115ec3e2fb4 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Tue, 15 Mar 2022 01:24:14 +0530 Subject: [PATCH 01/36] invite email modal added --- dashboard/package.json | 1 + dashboard/src/components/InviteEmailModal.tsx | 254 ++++++++++++++++++ dashboard/src/constants.ts | 7 + dashboard/src/pages/Users.tsx | 2 + dashboard/src/utils/index.ts | 11 + 5 files changed, 275 insertions(+) create mode 100644 dashboard/src/components/InviteEmailModal.tsx diff --git a/dashboard/package.json b/dashboard/package.json index af68d43..9f854ba 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -24,6 +24,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-dropzone": "^12.0.4", "react-icons": "^4.3.1", "react-router-dom": "^6.2.1", "typescript": "^4.5.4", diff --git a/dashboard/src/components/InviteEmailModal.tsx b/dashboard/src/components/InviteEmailModal.tsx new file mode 100644 index 0000000..7f69bd5 --- /dev/null +++ b/dashboard/src/components/InviteEmailModal.tsx @@ -0,0 +1,254 @@ +import React, { useState, useCallback } from 'react'; +import { + Button, + Center, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + useToast, + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, + InputGroup, + Input, + InputRightElement, + Text, + Link, +} from '@chakra-ui/react'; +import { useClient } from 'urql'; +import { FaUserPlus, FaMinusCircle, FaPlus, FaUpload } from 'react-icons/fa'; +import { useDropzone } from 'react-dropzone'; +import { escape } from 'lodash'; +import { validateEmail } from '../utils'; +import { UpdateUser } from '../graphql/mutation'; +import { ArrayInputOperations, csvDemoData } from '../constants'; + +interface emailDataTypes { + value: string; + isInvalid: boolean; +} + +const InviteEmailModal = () => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [emails, setEmails] = useState([ + { + value: '', + isInvalid: false, + }, + { + value: '', + isInvalid: false, + }, + { + value: '', + isInvalid: false, + }, + { + value: '', + isInvalid: false, + }, + ]); + const sendInviteHandler = async () => { + onClose(); + }; + const updateEmailListHandler = (operation: string, index: number = 0) => { + switch (operation) { + case ArrayInputOperations.APPEND: + setEmails([ + ...emails, + { + value: '', + isInvalid: false, + }, + ]); + break; + case ArrayInputOperations.REMOVE: + const updatedEmailList = [...emails]; + updatedEmailList.splice(index, 1); + setEmails(updatedEmailList); + break; + default: + break; + } + }; + const inputChangeHandler = (value: string, index: number) => { + const updatedEmailList = [...emails]; + updatedEmailList[index].value = value; + updatedEmailList[index].isInvalid = !validateEmail(value); + setEmails(updatedEmailList); + }; + const onDrop = useCallback((acceptedFiles) => { + console.log(acceptedFiles); + }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + return ( + <> + + + + + Invite Email + + + + + Enter emails + Upload CSV + + + + + + Emails + + + + + {emails.map((emailData, index) => ( + + + + inputChangeHandler(e.currentTarget.value, index) + } + /> + + + + + + ))} + + + + + + {isDragActive ? ( + Drop the files here... + ) : ( + +
+ +
+ + Drag 'n' drop the csv file here, or click to select. + + + Download{' '} + e.stopPropagation()} + > + {' '} + sample.csv + {' '} + and modify it.{' '} + +
+ )} +
+
+
+
+
+ + + +
+
+ + ); +}; + +export default InviteEmailModal; diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 64fa372..6900730 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -88,3 +88,10 @@ export const ECDSAEncryptionType = { ES384: 'ES384', ES512: 'ES512', }; + +export const csvDemoData = `email +lakhan.demo@contentment.org +john@gmail.com +anik@contentment.org +harry@potter.com +anikgh89@gmail.com`; diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 5d36c5b..9b9e512 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -42,6 +42,7 @@ import { UserDetailsQuery } from '../graphql/queries'; import { UpdateUser } from '../graphql/mutation'; import EditUserModal from '../components/EditUserModal'; import DeleteUserModal from '../components/DeleteUserModal'; +import InviteEmailModal from '../components/InviteEmailModal'; interface paginationPropTypes { limit: number; @@ -177,6 +178,7 @@ export default function Users() { Users + {!loading ? ( userList.length > 0 ? ( diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts index 64ca0bf..60f4906 100644 --- a/dashboard/src/utils/index.ts +++ b/dashboard/src/utils/index.ts @@ -64,3 +64,14 @@ export const getObjectDiff = (obj1: any, obj2: any) => { return diff; }; + +export const validateEmail = (email: string) => { + if (!email || email === '') return true; + return email + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ) + ? true + : false; +}; From 9a19552f7236c724a9cf33e0e74c6845a0cfe7ee Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 08:53:48 +0530 Subject: [PATCH 02/36] feat: add resolver for inviting members --- app/src/Root.tsx | 4 + app/src/pages/setup-password.tsx | 12 +++ server/email/invite_email.go | 113 ++++++++++++++++++++++ server/graph/generated/generated.go | 118 +++++++++++++++++++++++ server/graph/model/models_gen.go | 5 + server/graph/schema.graphqls | 6 ++ server/graph/schema.resolvers.go | 4 + server/resolvers/forgot_password.go | 2 +- server/resolvers/invite_members.go | 134 +++++++++++++++++++++++++++ server/resolvers/magic_link_login.go | 2 +- server/resolvers/signup.go | 2 +- server/resolvers/update_profile.go | 2 +- server/resolvers/update_user.go | 2 +- server/utils/urls.go | 11 +++ 14 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 app/src/pages/setup-password.tsx create mode 100644 server/email/invite_email.go create mode 100644 server/resolvers/invite_members.go diff --git a/app/src/Root.tsx b/app/src/Root.tsx index d62ded8..8707a2b 100644 --- a/app/src/Root.tsx +++ b/app/src/Root.tsx @@ -1,6 +1,7 @@ import React, { useEffect, lazy, Suspense } from 'react'; import { Switch, Route } from 'react-router-dom'; import { useAuthorizer } from '@authorizerdev/authorizer-react'; +import SetupPassword from './pages/setup-password'; const ResetPassword = lazy(() => import('./pages/rest-password')); const Login = lazy(() => import('./pages/login')); @@ -60,6 +61,9 @@ export default function Root({ + + + ); diff --git a/app/src/pages/setup-password.tsx b/app/src/pages/setup-password.tsx new file mode 100644 index 0000000..1709797 --- /dev/null +++ b/app/src/pages/setup-password.tsx @@ -0,0 +1,12 @@ +import React, { Fragment } from 'react'; +import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react'; + +export default function SetupPassword() { + return ( + +

Setup new Password

+
+ +
+ ); +} diff --git a/server/email/invite_email.go b/server/email/invite_email.go new file mode 100644 index 0000000..23a5cf3 --- /dev/null +++ b/server/email/invite_email.go @@ -0,0 +1,113 @@ +package email + +import ( + "log" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/envstore" +) + +// InviteEmail to send invite email +func InviteEmail(toEmail, token, url string) error { + // The receiver needs to be in slice as the receive supports multiple receiver + Receiver := []string{toEmail} + + Subject := "Please accept the invitation" + message := ` + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+
+
+ + + ` + data := make(map[string]interface{}, 3) + data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) + data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) + data["verification_url"] = url + "?token=" + token + message = addEmailTemplate(message, data, "verify_email.tmpl") + // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) + + err := SendMail(Receiver, Subject, message) + if err != nil { + log.Println("=> error sending email:", err) + } + return err +} diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 817dd79..43d5c21 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -114,6 +114,7 @@ type ComplexityRoot struct { AdminSignup func(childComplexity int, params model.AdminSignupInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int + InviteMembers func(childComplexity int, params model.InviteMemberInput) int Login func(childComplexity int, params model.LoginInput) int Logout func(childComplexity int) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int @@ -208,6 +209,7 @@ type MutationResolver interface { AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error) AdminLogout(ctx context.Context) (*model.Response, error) UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error) + InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -660,6 +662,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true + case "Mutation._invite_members": + if e.complexity.Mutation.InviteMembers == nil { + break + } + + args, err := ec.field_Mutation__invite_members_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.InviteMembers(childComplexity, args["params"].(model.InviteMemberInput)), true + case "Mutation.login": if e.complexity.Mutation.Login == nil { break @@ -1434,6 +1448,11 @@ input OAuthRevokeInput { refresh_token: String! } +input InviteMemberInput { + emails: [String!]! + redirect_uri: String +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -1452,6 +1471,7 @@ type Mutation { _admin_login(params: AdminLoginInput!): Response! _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! + _invite_members(params: InviteMemberInput!): Response! } type Query { @@ -1517,6 +1537,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.InviteMemberInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4182,6 +4217,48 @@ func (ec *executionContext) _Mutation__update_env(ctx context.Context, field gra return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation__invite_members_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().InviteMembers(rctx, args["params"].(model.InviteMemberInput)) + }) + 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) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6914,6 +6991,37 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex return it, nil } +func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) { + var it model.InviteMemberInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "emails": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emails")) + it.Emails, err = ec.unmarshalNString2ᚕstringᚄ(ctx, v) + if err != nil { + return it, err + } + case "redirect_uri": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri")) + it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj interface{}) (model.LoginInput, error) { var it model.LoginInput asMap := map[string]interface{}{} @@ -8182,6 +8290,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_invite_members": + out.Values[i] = ec._Mutation__invite_members(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8911,6 +9024,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel return res } +func (ec *executionContext) unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx context.Context, v interface{}) (model.InviteMemberInput, error) { + res, err := ec.unmarshalInputInviteMemberInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) { res, err := ec.unmarshalInputLoginInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ea069e5..88be349 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -74,6 +74,11 @@ type ForgotPasswordInput struct { RedirectURI *string `json:"redirect_uri"` } +type InviteMemberInput struct { + Emails []string `json:"emails"` + RedirectURI *string `json:"redirect_uri"` +} + type LoginInput struct { Email string `json:"email"` Password string `json:"password"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 18a727c..fcd2db0 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -272,6 +272,11 @@ input OAuthRevokeInput { refresh_token: String! } +input InviteMemberInput { + emails: [String!]! + redirect_uri: String +} + type Mutation { signup(params: SignUpInput!): AuthResponse! login(params: LoginInput!): AuthResponse! @@ -290,6 +295,7 @@ type Mutation { _admin_login(params: AdminLoginInput!): Response! _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! + _invite_members(params: InviteMemberInput!): Response! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 245d7d8..e4f9275 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -75,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv return resolvers.UpdateEnvResolver(ctx, params) } +func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { + return resolvers.InviteMembersResolver(ctx, params) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.MetaResolver(ctx) } diff --git a/server/resolvers/forgot_password.go b/server/resolvers/forgot_password.go index 4be96b1..58edb08 100644 --- a/server/resolvers/forgot_password.go +++ b/server/resolvers/forgot_password.go @@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu if err != nil { return res, err } - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) + "/reset-password" if params.RedirectURI != nil { redirectURL = *params.RedirectURI } diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go new file mode 100644 index 0000000..649e36a --- /dev/null +++ b/server/resolvers/invite_members.go @@ -0,0 +1,134 @@ +package resolvers + +import ( + "context" + "errors" + "log" + "strings" + "time" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/db/models" + emailservice "github.com/authorizerdev/authorizer/server/email" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// InviteMembersResolver resolver to invite members +func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, errors.New("unauthorized") + } + + // this feature is only allowed if email server is configured + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { + return res, errors.New("email sending is disabled") + } + + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { + return res, errors.New("either basic authentication or magic link login is required") + } + + // filter valid emails + emails := []string{} + for _, email := range params.Emails { + if utils.IsValidEmail(email) { + emails = append(emails, email) + } + } + + if len(emails) == 0 { + res.Message = "No valid emails found" + return res, errors.New("no valid emails found") + } + + // TODO: optimise to use like query instead of looping through emails and getting user individually + // for each emails check if emails exists in db + newEmails := []string{} + for _, email := range emails { + _, err := db.Provider.GetUserByEmail(email) + if err != nil { + log.Printf("%s user not found. inviting user.", email) + newEmails = append(newEmails, email) + } else { + log.Println("%s user already exists. skipping.", email) + } + } + + if len(newEmails) == 0 { + res.Message = "All emails already exist" + return res, errors.New("all emails already exist") + } + + // invite new emails + for _, email := range newEmails { + + user := models.User{ + Email: email, + Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","), + } + redirectURL := utils.GetAppURL(gc) + "/verify_email" + if params.RedirectURI != nil { + redirectURL = *params.RedirectURI + } + + _, nonceHash, err := utils.GenerateNonce() + if err != nil { + return res, err + } + + verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, redirectURL, nonceHash, redirectURL) + if err != nil { + log.Println(`error generating token`, err) + } + + verificationRequest := models.VerificationRequest{ + Token: verificationToken, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: email, + Nonce: nonceHash, + RedirectURI: redirectURL, + } + + // use magic link login if that option is on + if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { + user.SignupMethods = constants.SignupMethodMagicLinkLogin + verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin + } else { + // use basic authentication if that option is on + user.SignupMethods = constants.SignupMethodBasicAuth + verificationRequest.Identifier = constants.VerificationTypeForgotPassword + + redirectURL = utils.GetAppURL(gc) + "/setup-password" + if params.RedirectURI != nil { + redirectURL = *params.RedirectURI + } + + } + + user, err = db.Provider.AddUser(user) + if err != nil { + log.Printf("error inviting user: %s, err: %v", email, err) + return res, err + } + + _, err = db.Provider.AddVerificationRequest(verificationRequest) + if err != nil { + log.Printf("error inviting user: %s, err: %v", email, err) + return res, err + } + + go emailservice.InviteEmail(email, verificationToken, redirectURL) + } + + return res, nil +} diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index 7014ca1..05d0708 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -123,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu if params.Scope != nil && len(params.Scope) > 0 { redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ") } - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) if params.RedirectURI != nil { redirectURL = *params.RedirectURI } diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 308d284..478f8b7 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -128,7 +128,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR return res, err } verificationType := constants.VerificationTypeBasicAuthSignup - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL) if err != nil { return res, err diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index 5a3f328..73e87fe 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) return res, err } verificationType := constants.VerificationTypeUpdateEmail - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index f9438d4..16871cf 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod return res, err } verificationType := constants.VerificationTypeUpdateEmail - redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + redirectURL := utils.GetAppURL(gc) verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) diff --git a/server/utils/urls.go b/server/utils/urls.go index 64b8406..e4e6c06 100644 --- a/server/utils/urls.go +++ b/server/utils/urls.go @@ -4,6 +4,8 @@ import ( "net/url" "strings" + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/envstore" "github.com/gin-gonic/gin" ) @@ -71,3 +73,12 @@ func GetDomainName(uri string) string { return host } + +// GetAppURL to get /app/ url if not configured by user +func GetAppURL(gc *gin.Context) string { + envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) + if envAppURL == "" { + envAppURL = GetHost(gc) + "/app/" + } + return envAppURL +} From 3e7150f872555857740d052e6ed3b019e19adc6f Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 09:56:50 +0530 Subject: [PATCH 03/36] fix: redirect uri --- app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 1131b59..e81ebba 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -21,7 +21,7 @@ export default function App() { if (redirectURL) { urlProps.redirectURL = redirectURL; } else { - urlProps.redirectURL = window.location.origin; + urlProps.redirectURL = window.location.origin + '/app'; } const globalState: Record = { // @ts-ignore From 5e6ee8d9b0c3af61f714fcb636e2f60d503a2986 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 09:57:09 +0530 Subject: [PATCH 04/36] fix: setup-password flow --- server/email/invite_email.go | 2 +- server/resolvers/invite_members.go | 41 +++++++++++++++--------------- server/resolvers/session.go | 8 ++++-- server/test/invite_member_test.go | 1 + server/utils/urls.go | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 server/test/invite_member_test.go diff --git a/server/email/invite_email.go b/server/email/invite_email.go index 23a5cf3..7db1a0a 100644 --- a/server/email/invite_email.go +++ b/server/email/invite_email.go @@ -102,7 +102,7 @@ func InviteEmail(toEmail, token, url string) error { data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) data["verification_url"] = url + "?token=" + token - message = addEmailTemplate(message, data, "verify_email.tmpl") + message = addEmailTemplate(message, data, "invite_email.tmpl") // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) err := SendMail(Receiver, Subject, message) diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 649e36a..817cdd1 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -3,6 +3,7 @@ package resolvers import ( "context" "errors" + "fmt" "log" "strings" "time" @@ -20,22 +21,21 @@ import ( // InviteMembersResolver resolver to invite members func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) { gc, err := utils.GinContextFromContext(ctx) - var res *model.Response if err != nil { - return res, err + return nil, err } if !token.IsSuperAdmin(gc) { - return res, errors.New("unauthorized") + return nil, errors.New("unauthorized") } // this feature is only allowed if email server is configured if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { - return res, errors.New("email sending is disabled") + return nil, errors.New("email sending is disabled") } if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) { - return res, errors.New("either basic authentication or magic link login is required") + return nil, errors.New("either basic authentication or magic link login is required") } // filter valid emails @@ -47,8 +47,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } if len(emails) == 0 { - res.Message = "No valid emails found" - return res, errors.New("no valid emails found") + return nil, errors.New("no valid emails found") } // TODO: optimise to use like query instead of looping through emails and getting user individually @@ -65,8 +64,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) } if len(newEmails) == 0 { - res.Message = "All emails already exist" - return res, errors.New("all emails already exist") + return nil, errors.New("all emails already exist") } // invite new emails @@ -76,17 +74,21 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) Email: email, Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","), } - redirectURL := utils.GetAppURL(gc) + "/verify_email" + hostname := utils.GetHost(gc) + verifyEmailURL := hostname + "/verify_email" + appURL := utils.GetAppURL(gc) + + redirectURL := appURL if params.RedirectURI != nil { redirectURL = *params.RedirectURI } _, nonceHash, err := utils.GenerateNonce() if err != nil { - return res, err + return nil, err } - verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, redirectURL, nonceHash, redirectURL) + verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) } @@ -108,27 +110,26 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) user.SignupMethods = constants.SignupMethodBasicAuth verificationRequest.Identifier = constants.VerificationTypeForgotPassword - redirectURL = utils.GetAppURL(gc) + "/setup-password" - if params.RedirectURI != nil { - redirectURL = *params.RedirectURI - } + verifyEmailURL = appURL + "/setup-password" } user, err = db.Provider.AddUser(user) if err != nil { log.Printf("error inviting user: %s, err: %v", email, err) - return res, err + return nil, err } _, err = db.Provider.AddVerificationRequest(verificationRequest) if err != nil { log.Printf("error inviting user: %s, err: %v", email, err) - return res, err + return nil, err } - go emailservice.InviteEmail(email, verificationToken, redirectURL) + go emailservice.InviteEmail(email, verificationToken, verifyEmailURL) } - return res, nil + return &model.Response{ + Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)), + }, nil } diff --git a/server/resolvers/session.go b/server/resolvers/session.go index 151321d..e68fe07 100644 --- a/server/resolvers/session.go +++ b/server/resolvers/session.go @@ -2,7 +2,9 @@ package resolvers import ( "context" + "errors" "fmt" + "log" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" @@ -24,13 +26,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod sessionToken, err := cookie.GetSession(gc) if err != nil { - return res, err + log.Println("error getting session token:", err) + return res, errors.New("unauthorized") } // get session from cookie claims, err := token.ValidateBrowserSession(gc, sessionToken) if err != nil { - return res, err + log.Println("session validation failed:", err) + return res, errors.New("unauthorized") } userID := claims.Subject user, err := db.Provider.GetUserByID(userID) diff --git a/server/test/invite_member_test.go b/server/test/invite_member_test.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/server/test/invite_member_test.go @@ -0,0 +1 @@ +package test diff --git a/server/utils/urls.go b/server/utils/urls.go index e4e6c06..390cfbd 100644 --- a/server/utils/urls.go +++ b/server/utils/urls.go @@ -78,7 +78,7 @@ func GetDomainName(uri string) string { func GetAppURL(gc *gin.Context) string { envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL) if envAppURL == "" { - envAppURL = GetHost(gc) + "/app/" + envAppURL = GetHost(gc) + "/app" } return envAppURL } From 74a8024131c67d73b920b09ffac75d0f5513888a Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Tue, 15 Mar 2022 12:09:54 +0530 Subject: [PATCH 05/36] feat: add integration test for invite_member --- server/test/invite_member_test.go | 57 +++++++++++++++++++++++++++++++ server/test/resolvers_test.go | 1 + 2 files changed, 58 insertions(+) diff --git a/server/test/invite_member_test.go b/server/test/invite_member_test.go index 56e5404..76cd389 100644 --- a/server/test/invite_member_test.go +++ b/server/test/invite_member_test.go @@ -1 +1,58 @@ package test + +import ( + "fmt" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/crypto" + "github.com/authorizerdev/authorizer/server/envstore" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func inviteUserTest(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should invite user successfully`, func(t *testing.T) { + req, ctx := createContext(s) + emails := []string{"invite_member1." + s.TestInfo.Email} + + // unauthorized error + res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + + assert.Error(t, err) + assert.Nil(t, res) + + h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)) + assert.Nil(t, err) + req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h)) + + // invalid emails test + invalidEmailsTest := []string{ + "test", + "test.com", + } + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: invalidEmailsTest, + }) + + // valid test + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + assert.Nil(t, err) + assert.NotNil(t, res) + + // duplicate error test + res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{ + Emails: emails, + }) + assert.Error(t, err) + assert.Nil(t, res) + + cleanData(emails[0]) + }) +} diff --git a/server/test/resolvers_test.go b/server/test/resolvers_test.go index bc8eedc..7e0c41d 100644 --- a/server/test/resolvers_test.go +++ b/server/test/resolvers_test.go @@ -62,6 +62,7 @@ func TestResolvers(t *testing.T) { magicLinkLoginTests(t, s) logoutTests(t, s) metaTests(t, s) + inviteUserTest(t, s) }) } } From e126bfddadf50a4b6aab03a4150555cf2e54c477 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Tue, 15 Mar 2022 20:31:54 +0530 Subject: [PATCH 06/36] invite email modal updated --- dashboard/src/components/InviteEmailModal.tsx | 69 ++++++++++++++----- dashboard/src/utils/index.ts | 11 +++ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/dashboard/src/components/InviteEmailModal.tsx b/dashboard/src/components/InviteEmailModal.tsx index 7f69bd5..33bb792 100644 --- a/dashboard/src/components/InviteEmailModal.tsx +++ b/dashboard/src/components/InviteEmailModal.tsx @@ -27,11 +27,11 @@ import { useClient } from 'urql'; import { FaUserPlus, FaMinusCircle, FaPlus, FaUpload } from 'react-icons/fa'; import { useDropzone } from 'react-dropzone'; import { escape } from 'lodash'; -import { validateEmail } from '../utils'; +import { validateEmail, validateURI } from '../utils'; import { UpdateUser } from '../graphql/mutation'; import { ArrayInputOperations, csvDemoData } from '../constants'; -interface emailDataTypes { +interface stateDataTypes { value: string; isInvalid: boolean; } @@ -40,19 +40,12 @@ const InviteEmailModal = () => { const client = useClient(); const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); - const [emails, setEmails] = useState([ - { - value: '', - isInvalid: false, - }, - { - value: '', - isInvalid: false, - }, - { - value: '', - isInvalid: false, - }, + const [tabIndex, setTabIndex] = useState(0); + const [redirectURI, setRedirectURI] = useState({ + value: '', + isInvalid: false, + }); + const [emails, setEmails] = useState([ { value: '', isInvalid: false, @@ -90,6 +83,18 @@ const InviteEmailModal = () => { const onDrop = useCallback((acceptedFiles) => { console.log(acceptedFiles); }, []); + const handleTabsChange = (index: number) => { + setTabIndex(index); + }; + const setRedirectURIHandler = (value: string) => { + const updatedRedirectURI: stateDataTypes = { + value: '', + isInvalid: false, + }; + updatedRedirectURI.value = value; + updatedRedirectURI.isInvalid = !validateURI(value); + setRedirectURI(updatedRedirectURI); + }; const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); return ( <> @@ -111,7 +116,12 @@ const InviteEmailModal = () => { Invite Email - + Enter emails Upload CSV @@ -124,6 +134,33 @@ const InviteEmailModal = () => { > + + Redirect URI + + + + + setRedirectURIHandler(e.currentTarget.value) + } + /> + + { ? true : false; }; + +export const validateURI = (uri: string) => { + if (!uri || uri === '') return true; + return uri + .toLowerCase() + .match( + /(?:^|\s)((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/ + ) + ? true + : false; +}; From 2913fa06035b18c61fe8c2a121c6203c63200679 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Tue, 15 Mar 2022 23:51:54 +0530 Subject: [PATCH 07/36] updates --- dashboard/src/components/InviteEmailModal.tsx | 26 ++++++++++--- dashboard/src/constants.ts | 3 +- dashboard/src/utils/parseCSV.ts | 39 +++++++++++++++++++ 3 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 dashboard/src/utils/parseCSV.ts diff --git a/dashboard/src/components/InviteEmailModal.tsx b/dashboard/src/components/InviteEmailModal.tsx index 33bb792..7cd9ad2 100644 --- a/dashboard/src/components/InviteEmailModal.tsx +++ b/dashboard/src/components/InviteEmailModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Button, Center, @@ -30,6 +30,7 @@ import { escape } from 'lodash'; import { validateEmail, validateURI } from '../utils'; import { UpdateUser } from '../graphql/mutation'; import { ArrayInputOperations, csvDemoData } from '../constants'; +import parseCSV from '../utils/parseCSV'; interface stateDataTypes { value: string; @@ -51,6 +52,16 @@ const InviteEmailModal = () => { isInvalid: false, }, ]); + const [disableSendButton, setDisableSendButton] = useState(false); + useEffect(() => { + if (redirectURI.isInvalid) { + setDisableSendButton(true); + } else if (emails.some((emailData) => emailData.isInvalid)) { + setDisableSendButton(true); + } else { + setDisableSendButton(false); + } + }, [redirectURI, emails]); const sendInviteHandler = async () => { onClose(); }; @@ -80,8 +91,10 @@ const InviteEmailModal = () => { updatedEmailList[index].isInvalid = !validateEmail(value); setEmails(updatedEmailList); }; - const onDrop = useCallback((acceptedFiles) => { - console.log(acceptedFiles); + const onDrop = useCallback(async (acceptedFiles) => { + const result = await parseCSV(acceptedFiles[0], ','); + setEmails(result); + setTabIndex(0); }, []); const handleTabsChange = (index: number) => { setTabIndex(index); @@ -95,7 +108,10 @@ const InviteEmailModal = () => { updatedRedirectURI.isInvalid = !validateURI(value); setRedirectURI(updatedRedirectURI); }; - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: 'text/csv', + }); return ( <> - Invite Email + Invite Members { ); }; -export default InviteEmailModal; +export default InviteMembersModal; diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 9b9e512..c98df5e 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -42,7 +42,7 @@ import { UserDetailsQuery } from '../graphql/queries'; import { UpdateUser } from '../graphql/mutation'; import EditUserModal from '../components/EditUserModal'; import DeleteUserModal from '../components/DeleteUserModal'; -import InviteEmailModal from '../components/InviteEmailModal'; +import InviteMembersModal from '../components/InviteMembersModal'; interface paginationPropTypes { limit: number; @@ -178,7 +178,7 @@ export default function Users() { Users - + {!loading ? ( userList.length > 0 ? ( From 8ec52a90f1a68c0ef66da1bd5a250fce5a742533 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 14:08:08 +0530 Subject: [PATCH 09/36] updates --- dashboard/src/components/InviteMembersModal.tsx | 14 +++++++------- dashboard/src/pages/Users.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index 9bc159f..50a4c2c 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -37,7 +37,7 @@ interface stateDataTypes { isInvalid: boolean; } -const InviteMembersModal = () => { +const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { const client = useClient(); const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -91,14 +91,14 @@ const InviteMembersModal = () => { updatedEmailList[index].isInvalid = !validateEmail(value); setEmails(updatedEmailList); }; + const changeTabsHandler = (index: number) => { + setTabIndex(index); + }; const onDrop = useCallback(async (acceptedFiles) => { const result = await parseCSV(acceptedFiles[0], ','); setEmails(result); - setTabIndex(0); + changeTabsHandler(0); }, []); - const handleTabsChange = (index: number) => { - setTabIndex(index); - }; const setRedirectURIHandler = (value: string) => { const updatedRedirectURI: stateDataTypes = { value: '', @@ -119,7 +119,7 @@ const InviteMembersModal = () => { colorScheme="blue" variant="solid" onClick={onOpen} - isDisabled={false} + isDisabled={disabled} size="sm" >
@@ -136,7 +136,7 @@ const InviteMembersModal = () => { isFitted variant="enclosed" index={tabIndex} - onChange={handleTabsChange} + onChange={changeTabsHandler} > Enter emails diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index c98df5e..552fbe8 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -178,7 +178,7 @@ export default function Users() { Users - + {!loading ? ( userList.length > 0 ? ( From 32f8c99a71b9fd7832039472d22f7467c8f6971f Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 14:08:22 +0530 Subject: [PATCH 10/36] updates --- .../src/components/InviteMembersModal.tsx | 35 ++++++++++++++++--- dashboard/src/graphql/queries/index.ts | 8 +++++ dashboard/src/pages/Users.tsx | 16 +++++++-- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index 50a4c2c..c154d28 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -62,8 +62,22 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { setDisableSendButton(false); } }, [redirectURI, emails]); + useEffect(() => { + return () => { + setRedirectURI({ + value: '', + isInvalid: false, + }); + setEmails([ + { + value: '', + isInvalid: false, + }, + ]); + }; + }, []); const sendInviteHandler = async () => { - onClose(); + closeModalHandler(); }; const updateEmailListHandler = (operation: string, index: number = 0) => { switch (operation) { @@ -112,6 +126,19 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { onDrop, accept: 'text/csv', }); + const closeModalHandler = () => { + setRedirectURI({ + value: '', + isInvalid: false, + }); + setEmails([ + { + value: '', + isInvalid: false, + }, + ]); + onClose(); + }; return ( <> - + Invite Members diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index 8528f3f..698452e 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -84,3 +84,11 @@ export const UserDetailsQuery = ` } } `; + +export const EmailVerificationQuery = ` + query { + _env{ + DISABLE_EMAIL_VERIFICATION + } + } +`; diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 552fbe8..7e542d1 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -38,7 +38,7 @@ import { FaExclamationCircle, FaAngleDown, } from 'react-icons/fa'; -import { UserDetailsQuery } from '../graphql/queries'; +import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries'; import { UpdateUser } from '../graphql/mutation'; import EditUserModal from '../components/EditUserModal'; import DeleteUserModal from '../components/DeleteUserModal'; @@ -102,6 +102,8 @@ export default function Users() { }); const [userList, setUserList] = React.useState([]); const [loading, setLoading] = React.useState(false); + const [disableInviteMembers, setDisableInviteMembers] = + React.useState(true); const updateUserList = async () => { setLoading(true); const { data } = await client @@ -133,8 +135,18 @@ export default function Users() { } setLoading(false); }; + const checkEmailVerification = async () => { + setLoading(true); + const { data } = await client.query(EmailVerificationQuery).toPromise(); + if (data?._env) { + const { DISABLE_EMAIL_VERIFICATION } = data._env; + setDisableInviteMembers(DISABLE_EMAIL_VERIFICATION); + } + setLoading(false); + }; React.useEffect(() => { updateUserList(); + checkEmailVerification(); }, []); React.useEffect(() => { updateUserList(); @@ -178,7 +190,7 @@ export default function Users() { Users - + {!loading ? ( userList.length > 0 ? ( From f65ea72944d3cb3b670cbe5eff10d35794cdb6f1 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 14:10:55 +0530 Subject: [PATCH 11/36] package-lock.json --- dashboard/package-lock.json | 71 +++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 864d9cb..7917cfa 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -22,6 +22,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-dropzone": "^12.0.4", "react-icons": "^4.3.1", "react-router-dom": "^6.2.1", "typescript": "^4.5.4", @@ -1251,6 +1252,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/babel-plugin-macros": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", @@ -1631,6 +1640,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/file-selector": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", + "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -1914,9 +1934,9 @@ } }, "node_modules/prop-types": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz", - "integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -1959,6 +1979,22 @@ "react": "17.0.2" } }, + "node_modules/react-dropzone": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", + "integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.4.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8" + } + }, "node_modules/react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", @@ -3226,6 +3262,11 @@ } } }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, "babel-plugin-macros": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", @@ -3478,6 +3519,14 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, + "file-selector": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", + "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==", + "requires": { + "tslib": "^2.0.3" + } + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -3707,9 +3756,9 @@ } }, "prop-types": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz", - "integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3743,6 +3792,16 @@ "scheduler": "^0.20.2" } }, + "react-dropzone": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", + "integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.4.0", + "prop-types": "^15.8.1" + } + }, "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", From 2213619ed5f382faae3ea07d60cd764158cef3d3 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 18:06:51 +0530 Subject: [PATCH 12/36] updates --- .../src/components/InviteMembersModal.tsx | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index c154d28..ed5b98f 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -224,42 +224,44 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { - {emails.map((emailData, index) => ( - - - - inputChangeHandler(e.currentTarget.value, index) - } - /> - - - - - - ))} + /> + + + + + + ))} + @@ -268,7 +270,7 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { align="center" textAlign="center" bg="#f0f0f0" - h={231} + h={230} p={50} m={2} borderRadius={5} From d709f53c47b7e3f86db854e167892462d1a279d5 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 20:13:18 +0530 Subject: [PATCH 13/36] updates --- .../src/components/InviteMembersModal.tsx | 94 +++++++++++++------ dashboard/src/graphql/mutation/index.ts | 8 ++ dashboard/src/pages/Users.tsx | 5 +- 3 files changed, 78 insertions(+), 29 deletions(-) diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index ed5b98f..d267875 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -28,7 +28,7 @@ import { FaUserPlus, FaMinusCircle, FaPlus, FaUpload } from 'react-icons/fa'; import { useDropzone } from 'react-dropzone'; import { escape } from 'lodash'; import { validateEmail, validateURI } from '../utils'; -import { UpdateUser } from '../graphql/mutation'; +import { InviteMembers } from '../graphql/mutation'; import { ArrayInputOperations, csvDemoData } from '../constants'; import parseCSV from '../utils/parseCSV'; @@ -37,22 +37,33 @@ interface stateDataTypes { isInvalid: boolean; } -const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { +interface requestParamTypes { + emails: string[]; + redirect_uri?: string; +} + +const initData: stateDataTypes = { + value: '', + isInvalid: false, +}; + +const InviteMembersModal = ({ + updateUserList, + disabled = true, +}: { + updateUserList: Function; + disabled: boolean; +}) => { const client = useClient(); const toast = useToast(); const { isOpen, onOpen, onClose } = useDisclosure(); const [tabIndex, setTabIndex] = useState(0); const [redirectURI, setRedirectURI] = useState({ - value: '', - isInvalid: false, + ...initData, }); - const [emails, setEmails] = useState([ - { - value: '', - isInvalid: false, - }, - ]); + const [emails, setEmails] = useState([{ ...initData }]); const [disableSendButton, setDisableSendButton] = useState(false); + const [loading, setLoading] = React.useState(false); useEffect(() => { if (redirectURI.isInvalid) { setDisableSendButton(true); @@ -64,31 +75,58 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { }, [redirectURI, emails]); useEffect(() => { return () => { - setRedirectURI({ - value: '', - isInvalid: false, - }); - setEmails([ - { - value: '', - isInvalid: false, - }, - ]); + setRedirectURI({ ...initData }); + setEmails([{ ...initData }]); }; }, []); const sendInviteHandler = async () => { + setLoading(true); + try { + const emailList = emails + .filter((emailData) => !emailData.isInvalid) + .map((emailData) => emailData.value); + const params: requestParamTypes = { + emails: emailList, + }; + if (redirectURI.value !== '' && !redirectURI.isInvalid) { + params.redirect_uri = redirectURI.value; + } + if (emailList.length > 1) { + const res = await client + .mutation(InviteMembers, { + params, + }) + .toPromise(); + if (res.error) { + throw new Error('Internal server error'); + return; + } + toast({ + title: 'Invites sent successfully!', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + setLoading(false); + updateUserList(); + } else { + throw new Error('Please add emails'); + } + } catch (error: any) { + toast({ + title: error?.message || 'Error occurred, try again!', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + setLoading(false); + } closeModalHandler(); }; const updateEmailListHandler = (operation: string, index: number = 0) => { switch (operation) { case ArrayInputOperations.APPEND: - setEmails([ - ...emails, - { - value: '', - isInvalid: false, - }, - ]); + setEmails([...emails, { ...initData }]); break; case ArrayInputOperations.REMOVE: const updatedEmailList = [...emails]; @@ -318,7 +356,7 @@ const InviteMembersModal = ({ disabled = true }: { disabled: boolean }) => { colorScheme="blue" variant="solid" onClick={sendInviteHandler} - isDisabled={disableSendButton} + isDisabled={disableSendButton || loading} >
Send diff --git a/dashboard/src/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index 9736a01..df5db93 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -45,3 +45,11 @@ export const DeleteUser = ` } } `; + +export const InviteMembers = ` + mutation inviteMembers($params: InviteMemberInput!) { + _invite_members(params: $params) { + message + } + } +`; diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 7e542d1..a9d1d05 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -190,7 +190,10 @@ export default function Users() { Users - + {!loading ? ( userList.length > 0 ? ( From df7837f44d32b2ebc40df7b9a81e3b9a394e28a7 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 20:22:24 +0530 Subject: [PATCH 14/36] updates --- dashboard/src/components/InviteMembersModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index d267875..cf18f21 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -91,7 +91,7 @@ const InviteMembersModal = ({ if (redirectURI.value !== '' && !redirectURI.isInvalid) { params.redirect_uri = redirectURI.value; } - if (emailList.length > 1) { + if (emailList.length > 0) { const res = await client .mutation(InviteMembers, { params, From 99b846811a5ed6970088affaf6331739f5aba76e Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 16 Mar 2022 21:44:57 +0530 Subject: [PATCH 15/36] fix: token + redirect --- .gitignore | 4 ++- app/package-lock.json | 30 +++++++++++------------ app/package.json | 2 +- server/db/models/user.go | 9 ++++--- server/db/models/verification_requests.go | 24 ++++++++++++------ server/graph/generated/generated.go | 9 +++++++ server/graph/model/models_gen.go | 1 + server/graph/schema.graphqls | 1 + server/handlers/authorize.go | 3 +++ server/handlers/oauth_login.go | 4 +++ server/handlers/token.go | 4 +-- server/resolvers/update_user.go | 6 +++-- 12 files changed, 65 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index d979474..927a4df 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ data.db .DS_Store .env.local *.tar.gz -.vscode/ \ No newline at end of file +.vscode/ +.yalc +yalc.lock \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index 08d19ca..35e752a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "latest", + "@authorizerdev/authorizer-react": "0.10.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -24,9 +24,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.4.0-beta.3", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz", - "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.5.0.tgz", + "integrity": "sha512-O7T275ry4fJznQObnjYHPXvOtTtbv91NNFPh/x1jGs5iOC8MWvpnd7lbLvcnKbs0vPnZmFTzEUx8kCW2Z0o9Hg==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -35,11 +35,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.9.0-beta.7", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz", - "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.10.0.tgz", + "integrity": "sha512-0z/i+ystihxRbqERi984EGV5S9VK95uA2GwjtUfl8pEx7PwrmQYq+iis39kn/fSHDGVkekIHFkm071QDbn4XkQ==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.4.0-beta.3", + "@authorizerdev/authorizer-js": "^0.5.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -829,19 +829,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.4.0-beta.3", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz", - "integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.5.0.tgz", + "integrity": "sha512-O7T275ry4fJznQObnjYHPXvOtTtbv91NNFPh/x1jGs5iOC8MWvpnd7lbLvcnKbs0vPnZmFTzEUx8kCW2Z0o9Hg==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.9.0-beta.7", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz", - "integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.10.0.tgz", + "integrity": "sha512-0z/i+ystihxRbqERi984EGV5S9VK95uA2GwjtUfl8pEx7PwrmQYq+iis39kn/fSHDGVkekIHFkm071QDbn4XkQ==", "requires": { - "@authorizerdev/authorizer-js": "^0.4.0-beta.3", + "@authorizerdev/authorizer-js": "^0.5.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index cd974d6..68ff7c2 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "latest", + "@authorizerdev/authorizer-react": "0.10.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/server/db/models/user.go b/server/db/models/user.go index aa51771..b4e9dfd 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -32,6 +32,9 @@ type User struct { func (user *User) AsAPIUser() *model.User { isEmailVerified := user.EmailVerifiedAt != nil isPhoneVerified := user.PhoneNumberVerifiedAt != nil + email := user.Email + createdAt := user.CreatedAt + updatedAt := user.UpdatedAt return &model.User{ ID: user.ID, Email: user.Email, @@ -41,14 +44,14 @@ func (user *User) AsAPIUser() *model.User { FamilyName: user.FamilyName, MiddleName: user.MiddleName, Nickname: user.Nickname, - PreferredUsername: &user.Email, + PreferredUsername: &email, Gender: user.Gender, Birthdate: user.Birthdate, PhoneNumber: user.PhoneNumber, PhoneNumberVerified: &isPhoneVerified, Picture: user.Picture, Roles: strings.Split(user.Roles, ","), - CreatedAt: &user.CreatedAt, - UpdatedAt: &user.UpdatedAt, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, } } diff --git a/server/db/models/verification_requests.go b/server/db/models/verification_requests.go index 7d94c6a..a1b30ad 100644 --- a/server/db/models/verification_requests.go +++ b/server/db/models/verification_requests.go @@ -17,15 +17,23 @@ type VerificationRequest struct { } func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { + token := v.Token + createdAt := v.CreatedAt + updatedAt := v.UpdatedAt + email := v.Email + nonce := v.Nonce + redirectURI := v.RedirectURI + expires := v.ExpiresAt + identifier := v.Identifier return &model.VerificationRequest{ ID: v.ID, - Token: &v.Token, - Identifier: &v.Identifier, - Expires: &v.ExpiresAt, - CreatedAt: &v.CreatedAt, - UpdatedAt: &v.UpdatedAt, - Email: &v.Email, - Nonce: &v.Nonce, - RedirectURI: &v.RedirectURI, + Token: &token, + Identifier: &identifier, + Expires: &expires, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + Email: &email, + Nonce: &nonce, + RedirectURI: &redirectURI, } } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 43d5c21..92150c2 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -1358,6 +1358,7 @@ input SignUpInput { confirm_password: String! roles: [String!] scope: [String!] + redirect_uri: String } input LoginInput { @@ -7415,6 +7416,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i if err != nil { return it, err } + case "redirect_uri": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri")) + it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } } } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 88be349..9c02828 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -159,6 +159,7 @@ type SignUpInput struct { ConfirmPassword string `json:"confirm_password"` Roles []string `json:"roles"` Scope []string `json:"scope"` + RedirectURI *string `json:"redirect_uri"` } type UpdateEnvInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index fcd2db0..fdea73d 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -182,6 +182,7 @@ input SignUpInput { confirm_password: String! roles: [String!] scope: [String!] + redirect_uri: String } input LoginInput { diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index 53c94ee..cfb913f 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "strconv" "strings" @@ -50,6 +51,8 @@ func AuthorizeHandler() gin.HandlerFunc { gc.JSON(400, gin.H{"error": "invalid response mode"}) } + fmt.Println("=> redirect URI:", redirectURI) + fmt.Println("=> state:", state) if redirectURI == "" { redirectURI = "/app" } diff --git a/server/handlers/oauth_login.go b/server/handlers/oauth_login.go index 87eff74..a2ce229 100644 --- a/server/handlers/oauth_login.go +++ b/server/handlers/oauth_login.go @@ -16,7 +16,11 @@ import ( func OAuthLoginHandler() gin.HandlerFunc { return func(c *gin.Context) { hostname := utils.GetHost(c) + // deprecating redirectURL instead use redirect_uri redirectURI := strings.TrimSpace(c.Query("redirectURL")) + if redirectURI == "" { + redirectURI = strings.TrimSpace(c.Query("redirect_uri")) + } roles := strings.TrimSpace(c.Query("roles")) state := strings.TrimSpace(c.Query("state")) scopeString := strings.TrimSpace(c.Query("scope")) diff --git a/server/handlers/token.go b/server/handlers/token.go index 45c66e7..13abbb9 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -110,8 +110,6 @@ func TokenHandler() gin.HandlerFunc { return } - // rollover the session for security - sessionstore.RemoveState(sessionDataSplit[1]) // validate session claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1]) if err != nil { @@ -121,6 +119,8 @@ func TokenHandler() gin.HandlerFunc { }) return } + // rollover the session for security + sessionstore.RemoveState(sessionDataSplit[1]) userID = claims.Subject roles = claims.Roles scope = claims.Scope diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index 16871cf..a759399 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -154,6 +154,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod return res, err } + createdAt := user.CreatedAt + updatedAt := user.UpdatedAt res = &model.User{ ID: params.ID, Email: user.Email, @@ -161,8 +163,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod GivenName: user.GivenName, FamilyName: user.FamilyName, Roles: strings.Split(user.Roles, ","), - CreatedAt: &user.CreatedAt, - UpdatedAt: &user.UpdatedAt, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, } return res, nil } From 9f09823c8b6f9d441f497c1817853a455e5a9f86 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Wed, 16 Mar 2022 21:52:45 +0530 Subject: [PATCH 16/36] feat: add redirect_uri for signup --- app/package-lock.json | 30 +++++++++++++++--------------- app/package.json | 2 +- server/resolvers/signup.go | 3 +++ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 35e752a..c563734 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "0.10.0", + "@authorizerdev/authorizer-react": "0.11.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -24,9 +24,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.5.0.tgz", - "integrity": "sha512-O7T275ry4fJznQObnjYHPXvOtTtbv91NNFPh/x1jGs5iOC8MWvpnd7lbLvcnKbs0vPnZmFTzEUx8kCW2Z0o9Hg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz", + "integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -35,11 +35,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.10.0.tgz", - "integrity": "sha512-0z/i+ystihxRbqERi984EGV5S9VK95uA2GwjtUfl8pEx7PwrmQYq+iis39kn/fSHDGVkekIHFkm071QDbn4XkQ==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz", + "integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.5.0", + "@authorizerdev/authorizer-js": "^0.6.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -829,19 +829,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.5.0.tgz", - "integrity": "sha512-O7T275ry4fJznQObnjYHPXvOtTtbv91NNFPh/x1jGs5iOC8MWvpnd7lbLvcnKbs0vPnZmFTzEUx8kCW2Z0o9Hg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz", + "integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.10.0.tgz", - "integrity": "sha512-0z/i+ystihxRbqERi984EGV5S9VK95uA2GwjtUfl8pEx7PwrmQYq+iis39kn/fSHDGVkekIHFkm071QDbn4XkQ==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz", + "integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==", "requires": { - "@authorizerdev/authorizer-js": "^0.5.0", + "@authorizerdev/authorizer-js": "^0.6.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index 68ff7c2..afdfa15 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "0.10.0", + "@authorizerdev/authorizer-react": "0.11.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 478f8b7..0f49a87 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -129,6 +129,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR } verificationType := constants.VerificationTypeBasicAuthSignup redirectURL := utils.GetAppURL(gc) + if params.RedirectURI != nil { + redirectURL = *params.RedirectURI + } verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL) if err != nil { return res, err From 96edb43b6787eb86d9ba9f888f7096bf8d57dcc7 Mon Sep 17 00:00:00 2001 From: Anik Ghosh Date: Wed, 16 Mar 2022 22:49:18 +0530 Subject: [PATCH 17/36] feat: disable user signup --- dashboard/src/constants.ts | 1 + dashboard/src/graphql/queries/index.ts | 1 + dashboard/src/pages/Environment.tsx | 14 ++++ dashboard/src/pages/Users.tsx | 1 + server/constants/env.go | 2 + server/env/env.go | 1 + server/envstore/store.go | 1 + server/graph/generated/generated.go | 101 +++++++++++++++++++++++++ server/graph/model/models_gen.go | 3 + server/graph/schema.graphqls | 3 + server/handlers/oauth_callback.go | 4 + server/resolvers/env.go | 2 + server/resolvers/magic_link_login.go | 5 +- server/resolvers/signup.go | 5 ++ server/utils/meta.go | 1 + 15 files changed, 144 insertions(+), 1 deletion(-) diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index ead6fdd..582ca52 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -60,6 +60,7 @@ export const SwitchInputType = { DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN', DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION', DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION', + DISABLE_SIGN_UP: 'DISABLE_SIGN_UP', }; export const DateInputType = { diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index 698452e..fe35528 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -48,6 +48,7 @@ export const EnvVariablesQuery = ` DISABLE_MAGIC_LINK_LOGIN, DISABLE_EMAIL_VERIFICATION, DISABLE_BASIC_AUTHENTICATION, + DISABLE_SIGN_UP, CUSTOM_ACCESS_TOKEN_SCRIPT, DATABASE_NAME, DATABASE_TYPE, diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 06480bc..f3df0e6 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -68,6 +68,7 @@ interface envVarTypes { DISABLE_MAGIC_LINK_LOGIN: boolean; DISABLE_EMAIL_VERIFICATION: boolean; DISABLE_BASIC_AUTHENTICATION: boolean; + DISABLE_SIGN_UP: boolean; OLD_ADMIN_SECRET: string; DATABASE_NAME: string; DATABASE_TYPE: string; @@ -114,6 +115,7 @@ export default function Environment() { DISABLE_MAGIC_LINK_LOGIN: false, DISABLE_EMAIL_VERIFICATION: false, DISABLE_BASIC_AUTHENTICATION: false, + DISABLE_SIGN_UP: false, OLD_ADMIN_SECRET: '', DATABASE_NAME: '', DATABASE_TYPE: '', @@ -694,6 +696,18 @@ export default function Environment() { /> + + + Disable Sign Up: + + + + + diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index a9d1d05..09d8171 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -184,6 +184,7 @@ export default function Users() { } updateUserList(); }; + console.log('userList ==>> ', userList); return ( diff --git a/server/constants/env.go b/server/constants/env.go index 4206c37..f391750 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -67,6 +67,8 @@ const ( EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN" // EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE EnvKeyDisableLoginPage = "DISABLE_LOGIN_PAGE" + // EnvKeyDisableSignUp key for env variable DISABLE_SIGN_UP + EnvKeyDisableSignUp = "DISABLE_SIGN_UP" // EnvKeyRoles key for env variable ROLES EnvKeyRoles = "ROLES" // EnvKeyProtectedRoles key for env variable PROTECTED_ROLES diff --git a/server/env/env.go b/server/env/env.go index d430e2f..8770441 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -281,6 +281,7 @@ func InitAllEnv() error { envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true" envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true" envData.BoolEnv[constants.EnvKeyDisableLoginPage] = os.Getenv(constants.EnvKeyDisableLoginPage) == "true" + envData.BoolEnv[constants.EnvKeyDisableSignUp] = os.Getenv(constants.EnvKeyDisableSignUp) == "true" // no need to add nil check as its already done above if envData.StringEnv[constants.EnvKeySmtpHost] == "" || envData.StringEnv[constants.EnvKeySmtpUsername] == "" || envData.StringEnv[constants.EnvKeySmtpPassword] == "" || envData.StringEnv[constants.EnvKeySenderEmail] == "" && envData.StringEnv[constants.EnvKeySmtpPort] == "" { diff --git a/server/envstore/store.go b/server/envstore/store.go index c7cb122..e473615 100644 --- a/server/envstore/store.go +++ b/server/envstore/store.go @@ -41,6 +41,7 @@ var defaultStore = &EnvStore{ constants.EnvKeyDisableMagicLinkLogin: false, constants.EnvKeyDisableEmailVerification: false, constants.EnvKeyDisableLoginPage: false, + constants.EnvKeyDisableSignUp: false, }, SliceEnv: map[string][]string{}, }, diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 92150c2..3dd7e42 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -68,6 +68,7 @@ type ComplexityRoot struct { DisableEmailVerification func(childComplexity int) int DisableLoginPage func(childComplexity int) int DisableMagicLinkLogin func(childComplexity int) int + DisableSignUp func(childComplexity int) int FacebookClientID func(childComplexity int) int FacebookClientSecret func(childComplexity int) int GithubClientID func(childComplexity int) int @@ -105,6 +106,7 @@ type ComplexityRoot struct { IsGithubLoginEnabled func(childComplexity int) int IsGoogleLoginEnabled func(childComplexity int) int IsMagicLinkLoginEnabled func(childComplexity int) int + IsSignUpEnabled func(childComplexity int) int Version func(childComplexity int) int } @@ -383,6 +385,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true + case "Env.DISABLE_SIGN_UP": + if e.complexity.Env.DisableSignUp == nil { + break + } + + return e.complexity.Env.DisableSignUp(childComplexity), true + case "Env.FACEBOOK_CLIENT_ID": if e.complexity.Env.FacebookClientID == nil { break @@ -600,6 +609,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsMagicLinkLoginEnabled(childComplexity), true + case "Meta.is_sign_up_enabled": + if e.complexity.Meta.IsSignUpEnabled == nil { + break + } + + return e.complexity.Meta.IsSignUpEnabled(childComplexity), true + case "Meta.version": if e.complexity.Meta.Version == nil { break @@ -1197,6 +1213,7 @@ type Meta { is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! + is_sign_up_enabled: Boolean! } type User { @@ -1286,6 +1303,7 @@ type Env { DISABLE_BASIC_AUTHENTICATION: Boolean DISABLE_MAGIC_LINK_LOGIN: Boolean DISABLE_LOGIN_PAGE: Boolean + DISABLE_SIGN_UP: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -1322,6 +1340,7 @@ input UpdateEnvInput { DISABLE_BASIC_AUTHENTICATION: Boolean DISABLE_MAGIC_LINK_LOGIN: Boolean DISABLE_LOGIN_PAGE: Boolean + DISABLE_SIGN_UP: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -2826,6 +2845,38 @@ func (ec *executionContext) _Env_DISABLE_LOGIN_PAGE(ctx context.Context, field g return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) } +func (ec *executionContext) _Env_DISABLE_SIGN_UP(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.DisableSignUp, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ROLES(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3560,6 +3611,41 @@ func (ec *executionContext) _Meta_is_magic_link_login_enabled(ctx context.Contex return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Meta_is_sign_up_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Meta", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsSignUpEnabled, nil + }) + 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.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7607,6 +7693,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "DISABLE_SIGN_UP": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_SIGN_UP")) + it.DisableSignUp, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } case "ROLES": var err error @@ -8075,6 +8169,8 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Env_DISABLE_MAGIC_LINK_LOGIN(ctx, field, obj) case "DISABLE_LOGIN_PAGE": out.Values[i] = ec._Env_DISABLE_LOGIN_PAGE(ctx, field, obj) + case "DISABLE_SIGN_UP": + out.Values[i] = ec._Env_DISABLE_SIGN_UP(ctx, field, obj) case "ROLES": out.Values[i] = ec._Env_ROLES(ctx, field, obj) case "PROTECTED_ROLES": @@ -8193,6 +8289,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "is_sign_up_enabled": + out.Values[i] = ec._Meta_is_sign_up_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 9c02828..1ebad63 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -49,6 +49,7 @@ type Env struct { DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"` DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"` DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"` + DisableSignUp *bool `json:"DISABLE_SIGN_UP"` Roles []string `json:"ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"` DefaultRoles []string `json:"DEFAULT_ROLES"` @@ -103,6 +104,7 @@ type Meta struct { IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` + IsSignUpEnabled bool `json:"is_sign_up_enabled"` } type OAuthRevokeInput struct { @@ -184,6 +186,7 @@ type UpdateEnvInput struct { DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"` DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"` DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"` + DisableSignUp *bool `json:"DISABLE_SIGN_UP"` Roles []string `json:"ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"` DefaultRoles []string `json:"DEFAULT_ROLES"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index fdea73d..13f2a1b 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -21,6 +21,7 @@ type Meta { is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! + is_sign_up_enabled: Boolean! } type User { @@ -110,6 +111,7 @@ type Env { DISABLE_BASIC_AUTHENTICATION: Boolean DISABLE_MAGIC_LINK_LOGIN: Boolean DISABLE_LOGIN_PAGE: Boolean + DISABLE_SIGN_UP: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] @@ -146,6 +148,7 @@ input UpdateEnvInput { DISABLE_BASIC_AUTHENTICATION: Boolean DISABLE_MAGIC_LINK_LOGIN: Boolean DISABLE_LOGIN_PAGE: Boolean + DISABLE_SIGN_UP: Boolean ROLES: [String!] PROTECTED_ROLES: [String!] DEFAULT_ROLES: [String!] diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 1d28234..0c6ffd5 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -71,6 +71,10 @@ func OAuthCallbackHandler() gin.HandlerFunc { existingUser, err := db.Provider.GetUserByEmail(user.Email) if err != nil { + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) { + c.JSON(400, gin.H{"error": "signup is disabled for this instance"}) + return + } // user not registered, register user and generate session token user.SignupMethods = provider // make sure inputRoles don't include protected roles diff --git a/server/resolvers/env.go b/server/resolvers/env.go index 623d6b3..d56ea89 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -53,6 +53,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication] disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage] + disableSignUp := store.BoolEnv[constants.EnvKeyDisableSignUp] roles := store.SliceEnv[constants.EnvKeyRoles] defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles] protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles] @@ -92,6 +93,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { DisableBasicAuthentication: &disableBasicAuthentication, DisableMagicLinkLogin: &disableMagicLinkLogin, DisableLoginPage: &disableLoginPage, + DisableSignUp: &disableSignUp, Roles: roles, ProtectedRoles: protectedRoles, DefaultRoles: defaultRoles, diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index 05d0708..b69ea91 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -43,8 +43,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu // find user with email existingUser, err := db.Provider.GetUserByEmail(params.Email) - if err != nil { + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) { + return res, fmt.Errorf(`signup is disabled for this instance`) + } + user.SignupMethods = constants.SignupMethodMagicLinkLogin // define roles for new user if len(params.Roles) > 0 { diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 0f49a87..749f5a1 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -28,9 +28,14 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR return res, err } + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) { + return res, fmt.Errorf(`signup is disabled for this instance`) + } + if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) { return res, fmt.Errorf(`basic authentication is disabled for this instance`) } + if params.ConfirmPassword != params.Password { return res, fmt.Errorf(`password and confirm password does not match`) } diff --git a/server/utils/meta.go b/server/utils/meta.go index bf33250..18588c4 100644 --- a/server/utils/meta.go +++ b/server/utils/meta.go @@ -17,5 +17,6 @@ func GetMetaInfo() model.Meta { IsBasicAuthenticationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication), IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification), IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin), + IsSignUpEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp), } } From 69d781d6cf6ec4de621960eeee1b3eb006c5b6c4 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 17 Mar 2022 00:04:57 +0530 Subject: [PATCH 18/36] fix: set password re-direct uri --- app/package.json | 2 +- dashboard/src/pages/Users.tsx | 2 +- server/email/invite_email.go | 4 ++-- server/resolvers/invite_members.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/package.json b/app/package.json index afdfa15..cd974d6 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "0.11.0", + "@authorizerdev/authorizer-react": "latest", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index 09d8171..a231117 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -184,7 +184,7 @@ export default function Users() { } updateUserList(); }; - console.log('userList ==>> ', userList); + return ( diff --git a/server/email/invite_email.go b/server/email/invite_email.go index 7db1a0a..5cbd1c9 100644 --- a/server/email/invite_email.go +++ b/server/email/invite_email.go @@ -8,7 +8,7 @@ import ( ) // InviteEmail to send invite email -func InviteEmail(toEmail, token, url string) error { +func InviteEmail(toEmail, token, verificationURL, redirectURI string) error { // The receiver needs to be in slice as the receive supports multiple receiver Receiver := []string{toEmail} @@ -101,7 +101,7 @@ func InviteEmail(toEmail, token, url string) error { data := make(map[string]interface{}, 3) data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo) data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName) - data["verification_url"] = url + "?token=" + token + data["verification_url"] = verificationURL + "?token=" + token + "&redirect_uri=" + redirectURI message = addEmailTemplate(message, data, "invite_email.tmpl") // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go index 817cdd1..1a6a0cf 100644 --- a/server/resolvers/invite_members.go +++ b/server/resolvers/invite_members.go @@ -126,7 +126,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) return nil, err } - go emailservice.InviteEmail(email, verificationToken, verifyEmailURL) + go emailservice.InviteEmail(email, verificationToken, verifyEmailURL, redirectURL) } return &model.Response{ From 30be32a10b9099c90415ae414cb811c0b63e34f3 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 17 Mar 2022 00:15:47 +0530 Subject: [PATCH 19/36] feat: add sample csv --- dashboard/public/sample.csv | 1 + dashboard/src/components/InviteMembersModal.tsx | 6 ++---- dashboard/src/constants.ts | 6 ------ server/routes/routes.go | 1 + 4 files changed, 4 insertions(+), 10 deletions(-) create mode 100644 dashboard/public/sample.csv diff --git a/dashboard/public/sample.csv b/dashboard/public/sample.csv new file mode 100644 index 0000000..89fe4ab --- /dev/null +++ b/dashboard/public/sample.csv @@ -0,0 +1 @@ +foo@bar.com,test@authorizer.dev \ No newline at end of file diff --git a/dashboard/src/components/InviteMembersModal.tsx b/dashboard/src/components/InviteMembersModal.tsx index cf18f21..bd3642d 100644 --- a/dashboard/src/components/InviteMembersModal.tsx +++ b/dashboard/src/components/InviteMembersModal.tsx @@ -29,7 +29,7 @@ import { useDropzone } from 'react-dropzone'; import { escape } from 'lodash'; import { validateEmail, validateURI } from '../utils'; import { InviteMembers } from '../graphql/mutation'; -import { ArrayInputOperations, csvDemoData } from '../constants'; +import { ArrayInputOperations } from '../constants'; import parseCSV from '../utils/parseCSV'; interface stateDataTypes { @@ -332,9 +332,7 @@ const InviteMembersModal = ({ Download{' '} e.stopPropagation()} diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 582ca52..5fc9e5a 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -89,9 +89,3 @@ export const ECDSAEncryptionType = { ES384: 'ES384', ES512: 'ES512', }; - -export const csvDemoData = `lakhan.demo@contentment.org -john@gmail.com -anik@contentment.org -harry@potter.com -anikgh89@gmail.com`; diff --git a/server/routes/routes.go b/server/routes/routes.go index 71e1926..afde5d8 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -44,6 +44,7 @@ func InitRouter() *gin.Engine { { dashboard.Static("/favicon_io", "dashboard/favicon_io") dashboard.Static("/build", "dashboard/build") + dashboard.Static("/public", "dashboard/public") dashboard.GET("/", handlers.DashboardHandler()) dashboard.GET("/:page", handlers.DashboardHandler()) } From 3aa888b14eb44a221142c0d615b583bd1aea0726 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 17 Mar 2022 00:28:11 +0530 Subject: [PATCH 20/36] fix: use latest authorizer-react --- app/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/package-lock.json b/app/package-lock.json index c563734..1629608 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "0.11.0", + "@authorizerdev/authorizer-react": "latest", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", From d6f60ce464373f9db133bea43b20749fde83bd4a Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 17 Mar 2022 00:44:55 +0530 Subject: [PATCH 21/36] chore: add workflow-dispatch --- .github/workflows/release.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 74ce864..e30808b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,19 @@ on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + type: choice + options: + - info + - warning + - debug + tags: + description: 'Tags' + required: false + type: boolean release: types: [created] From 0c54da11681805cc5947d45a9e54d3a8ce82e07b Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 17 Mar 2022 09:31:40 +0530 Subject: [PATCH 22/36] fix: getting started --- README.md | 82 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9875f34..57c0fbf 100644 --- a/README.md +++ b/README.md @@ -59,35 +59,42 @@ # Getting Started -## Trying out Authorizer +## Step 1: Get Authorizer Instance + +### Deploy Production Ready Instance + +Deploy production ready Authorizer instance using one click deployment options available below + +| **Infra provider** | **One-click link** | **Additional information** | +| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: | +| Railway.app | Deploy on Railway | [docs](https://docs.authorizer.dev/deployment/railway) | +| Heroku | Deploy to Heroku | [docs](https://docs.authorizer.dev/deployment/heroku) | +| Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) | + +### Deploy Authorizer Using Source Code This guide helps you practice using Authorizer to evaluate it before you use it in a production environment. It includes instructions for installing the Authorizer server in local or standalone mode. -- [Install using source code](#install-using-source-code) -- [Install using binaries](#install-using-binaries) -- [Install instance on heroku](#install-instance-on-Heroku) -- [Install instance on railway.app](#install-instance-on-railway) +#### Install using source code -## Install using source code - -### Prerequisites +#### Prerequisites - OS: Linux or macOS or windows - Go: (Golang)(https://golang.org/dl/) >= v1.15 -### Project Setup +#### Project Setup 1. Fork the [authorizer](https://github.com/authorizerdev/authorizer) repository (**Skip this step if you have access to repo**) 2. Clone repo: `git clone https://github.com/authorizerdev/authorizer.git` or use the forked url from step 1 3. Change directory to authorizer: `cd authorizer` -5. Create Env file `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/) -6. Build Dashboard `make build-dashboard` -7. Build App `make build-app` -8. Build Server `make clean && make` +4. Create Env file `cp .env.sample .env`. Check all the supported env [here](https://docs.authorizer.dev/core/env/) +5. Build Dashboard `make build-dashboard` +6. Build App `make build-app` +7. Build Server `make clean && make` > Note: if you don't have [`make`](https://www.ibm.com/docs/en/aix/7.2?topic=concepts-make-command), you can `cd` into `server` dir and build using the `go build` command -9. Run binary `./build/server` +8. Run binary `./build/server` -## Install using binaries +### Deploy Authorizer using binaries Deploy / Try Authorizer using binaries. With each [Authorizer Release](https://github.com/authorizerdev/authorizer/releases) binaries are baked with required deployment files and bundled. You can download a specific version of it for the following operating systems: @@ -95,7 +102,7 @@ binaries are baked with required deployment files and bundled. You can download - Mac OSX - Linux -### Step 1: Download and unzip bundle +#### Download and unzip bundle - Download the Bundle for the specific OS from the [release page](https://github.com/authorizerdev/authorizer/releases) @@ -115,11 +122,7 @@ binaries are baked with required deployment files and bundled. You can download cd authorizer ``` -### Step 2: Configure environment variables - -Required environment variables are pre-configured in `.env` file. But based on the production requirements, please configure more environment variables. You can refer to [environment variables docs](/core/env) for more information. - -### Step 3: Start Authorizer +#### Step 3: Start Authorizer - Run following command to start authorizer @@ -131,20 +134,20 @@ Required environment variables are pre-configured in `.env` file. But based on t > Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server` -Deploy production ready Authorizer instance using one click deployment options available below +## Step 2: Setup Instance -| **Infra provider** | **One-click link** | **Additional information** | -| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: | -| Railway.app | Deploy on Railway | [docs](https://docs.authorizer.dev/deployment/railway) | -| Heroku | Deploy to Heroku | [docs](https://docs.authorizer.dev/deployment/heroku) | -| Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) | +- Open authorizer instance endpoint in browser +- Sign up as an admin with a secure password +- Configure environment variables from authorizer dashboard. Check env [docs](/core/env) for more information + +> Note: `DATABASE_URL`, `DATABASE_TYPE` and `DATABASE_NAME` are only configurable via platform envs ### Things to consider - For social logins, you will need respective social platform key and secret - For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it. > Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅 -- For persisting user sessions, you will need Redis URL (not in case of railway.app). If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server. +- For persisting user sessions, you will need Redis URL (not in case of railway app). If you do not configure a Redis server, sessions will be persisted until the instance is up or not restarted. For better response time on authorization requests/middleware, we recommend deploying Redis on the same infra/network as your authorizer server. ## Testing @@ -163,8 +166,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth