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
+}