feat: add support for magic link login (#65)
* feat: add support for magic link login * update readme
This commit is contained in:
parent
4269e2242c
commit
0305a719db
|
@ -30,6 +30,7 @@
|
||||||
- ✅ Forgot password flow using email
|
- ✅ Forgot password flow using email
|
||||||
- ✅ Social logins (Google, Github, Facebook, more coming soon)
|
- ✅ Social logins (Google, Github, Facebook, more coming soon)
|
||||||
- ✅ Role-based access management
|
- ✅ Role-based access management
|
||||||
|
- ✅ Password-less login with email and magic link
|
||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
|
@ -37,7 +38,6 @@
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Password-less login with email and magic link
|
|
||||||
- Support more JWT encryption algorithms (Currently supporting HS256)
|
- Support more JWT encryption algorithms (Currently supporting HS256)
|
||||||
- 2 Factor authentication
|
- 2 Factor authentication
|
||||||
- Back office (Admin dashboard to manage user)
|
- Back office (Admin dashboard to manage user)
|
||||||
|
|
|
@ -21,6 +21,7 @@ var (
|
||||||
RESET_PASSWORD_URL = ""
|
RESET_PASSWORD_URL = ""
|
||||||
DISABLE_EMAIL_VERIFICATION = "false"
|
DISABLE_EMAIL_VERIFICATION = "false"
|
||||||
DISABLE_BASIC_AUTHENTICATION = "false"
|
DISABLE_BASIC_AUTHENTICATION = "false"
|
||||||
|
DISABLE_MAGIC_LOGIN = "false"
|
||||||
|
|
||||||
// ROLES
|
// ROLES
|
||||||
ROLES = []string{}
|
ROLES = []string{}
|
||||||
|
|
|
@ -64,6 +64,7 @@ func InitEnv() {
|
||||||
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
|
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
|
||||||
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION")
|
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION")
|
||||||
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION")
|
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION")
|
||||||
|
constants.DISABLE_MAGIC_LOGIN = os.Getenv("DISABLE_MAGIC_LOGIN")
|
||||||
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
|
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
|
||||||
|
|
||||||
if constants.ADMIN_SECRET == "" {
|
if constants.ADMIN_SECRET == "" {
|
||||||
|
@ -126,6 +127,10 @@ func InitEnv() {
|
||||||
constants.DISABLE_BASIC_AUTHENTICATION = "false"
|
constants.DISABLE_BASIC_AUTHENTICATION = "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constants.DISABLE_MAGIC_LOGIN == "" {
|
||||||
|
constants.DISABLE_MAGIC_LOGIN = "false"
|
||||||
|
}
|
||||||
|
|
||||||
if constants.DISABLE_EMAIL_VERIFICATION == "" && constants.DISABLE_BASIC_AUTHENTICATION == "false" {
|
if constants.DISABLE_EMAIL_VERIFICATION == "" && constants.DISABLE_BASIC_AUTHENTICATION == "false" {
|
||||||
if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
|
if constants.SMTP_HOST == "" || constants.SENDER_EMAIL == "" || constants.SENDER_PASSWORD == "" {
|
||||||
constants.DISABLE_EMAIL_VERIFICATION = "true"
|
constants.DISABLE_EMAIL_VERIFICATION = "true"
|
||||||
|
|
|
@ -61,6 +61,7 @@ type ComplexityRoot struct {
|
||||||
IsFacebookLoginEnabled func(childComplexity int) int
|
IsFacebookLoginEnabled func(childComplexity int) int
|
||||||
IsGithubLoginEnabled func(childComplexity int) int
|
IsGithubLoginEnabled func(childComplexity int) int
|
||||||
IsGoogleLoginEnabled func(childComplexity int) int
|
IsGoogleLoginEnabled func(childComplexity int) int
|
||||||
|
IsMagicLoginEnabled func(childComplexity int) int
|
||||||
IsTwitterLoginEnabled func(childComplexity int) int
|
IsTwitterLoginEnabled func(childComplexity int) int
|
||||||
Version func(childComplexity int) int
|
Version func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
@ -71,6 +72,7 @@ type ComplexityRoot struct {
|
||||||
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
|
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
|
||||||
Login func(childComplexity int, params model.LoginInput) int
|
Login func(childComplexity int, params model.LoginInput) int
|
||||||
Logout func(childComplexity int) int
|
Logout func(childComplexity int) int
|
||||||
|
MagicLogin func(childComplexity int, params model.MagicLoginInput) int
|
||||||
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
|
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
|
||||||
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
|
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
|
||||||
Signup func(childComplexity int, params model.SignUpInput) int
|
Signup func(childComplexity int, params model.SignUpInput) int
|
||||||
|
@ -117,6 +119,7 @@ type ComplexityRoot struct {
|
||||||
type MutationResolver interface {
|
type MutationResolver interface {
|
||||||
Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, error)
|
Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, error)
|
||||||
Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, error)
|
Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, error)
|
||||||
|
MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error)
|
||||||
Logout(ctx context.Context) (*model.Response, error)
|
Logout(ctx context.Context) (*model.Response, error)
|
||||||
UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error)
|
UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model.Response, error)
|
||||||
AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error)
|
AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*model.User, error)
|
||||||
|
@ -226,6 +229,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Meta.IsGoogleLoginEnabled(childComplexity), true
|
return e.complexity.Meta.IsGoogleLoginEnabled(childComplexity), true
|
||||||
|
|
||||||
|
case "Meta.isMagicLoginEnabled":
|
||||||
|
if e.complexity.Meta.IsMagicLoginEnabled == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Meta.IsMagicLoginEnabled(childComplexity), true
|
||||||
|
|
||||||
case "Meta.isTwitterLoginEnabled":
|
case "Meta.isTwitterLoginEnabled":
|
||||||
if e.complexity.Meta.IsTwitterLoginEnabled == nil {
|
if e.complexity.Meta.IsTwitterLoginEnabled == nil {
|
||||||
break
|
break
|
||||||
|
@ -295,6 +305,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.Mutation.Logout(childComplexity), true
|
return e.complexity.Mutation.Logout(childComplexity), true
|
||||||
|
|
||||||
|
case "Mutation.magicLogin":
|
||||||
|
if e.complexity.Mutation.MagicLogin == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_Mutation_magicLogin_args(context.TODO(), rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Mutation.MagicLogin(childComplexity, args["params"].(model.MagicLoginInput)), true
|
||||||
|
|
||||||
case "Mutation.resendVerifyEmail":
|
case "Mutation.resendVerifyEmail":
|
||||||
if e.complexity.Mutation.ResendVerifyEmail == nil {
|
if e.complexity.Mutation.ResendVerifyEmail == nil {
|
||||||
break
|
break
|
||||||
|
@ -600,6 +622,7 @@ type Meta {
|
||||||
isGithubLoginEnabled: Boolean!
|
isGithubLoginEnabled: Boolean!
|
||||||
isEmailVerificationEnabled: Boolean!
|
isEmailVerificationEnabled: Boolean!
|
||||||
isBasicAuthenticationEnabled: Boolean!
|
isBasicAuthenticationEnabled: Boolean!
|
||||||
|
isMagicLoginEnabled: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
@ -699,9 +722,15 @@ input DeleteUserInput {
|
||||||
email: String!
|
email: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input MagicLoginInput {
|
||||||
|
email: String!
|
||||||
|
roles: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
signup(params: SignUpInput!): AuthResponse!
|
signup(params: SignUpInput!): AuthResponse!
|
||||||
login(params: LoginInput!): AuthResponse!
|
login(params: LoginInput!): AuthResponse!
|
||||||
|
magicLogin(params: MagicLoginInput!): Response!
|
||||||
logout: Response!
|
logout: Response!
|
||||||
updateProfile(params: UpdateProfileInput!): Response!
|
updateProfile(params: UpdateProfileInput!): Response!
|
||||||
adminUpdateUser(params: AdminUpdateUserInput!): User!
|
adminUpdateUser(params: AdminUpdateUserInput!): User!
|
||||||
|
@ -787,6 +816,21 @@ func (ec *executionContext) field_Mutation_login_args(ctx context.Context, rawAr
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_Mutation_magicLogin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]interface{}{}
|
||||||
|
var arg0 model.MagicLoginInput
|
||||||
|
if tmp, ok := rawArgs["params"]; ok {
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
|
||||||
|
arg0, err = ec.unmarshalNMagicLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMagicLoginInput(ctx, tmp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args["params"] = arg0
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Mutation_resendVerifyEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
func (ec *executionContext) field_Mutation_resendVerifyEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]interface{}{}
|
args := map[string]interface{}{}
|
||||||
|
@ -1376,6 +1420,41 @@ func (ec *executionContext) _Meta_isBasicAuthenticationEnabled(ctx context.Conte
|
||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Meta_isMagicLoginEnabled(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.IsMagicLoginEnabled, 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) {
|
func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -1460,6 +1539,48 @@ func (ec *executionContext) _Mutation_login(ctx context.Context, field graphql.C
|
||||||
return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
|
return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Mutation_magicLogin(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_magicLogin_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().MagicLogin(rctx, args["params"].(model.MagicLoginInput))
|
||||||
|
})
|
||||||
|
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) _Mutation_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_logout(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -3856,6 +3977,34 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalInputMagicLoginInput(ctx context.Context, obj interface{}) (model.MagicLoginInput, error) {
|
||||||
|
var it model.MagicLoginInput
|
||||||
|
var asMap = obj.(map[string]interface{})
|
||||||
|
|
||||||
|
for k, v := range asMap {
|
||||||
|
switch k {
|
||||||
|
case "email":
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
|
||||||
|
it.Email, err = ec.unmarshalNString2string(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
case "roles":
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles"))
|
||||||
|
it.Roles, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return it, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) {
|
func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) {
|
||||||
var it model.ResendVerifyEmailInput
|
var it model.ResendVerifyEmailInput
|
||||||
var asMap = obj.(map[string]interface{})
|
var asMap = obj.(map[string]interface{})
|
||||||
|
@ -4187,6 +4336,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "isMagicLoginEnabled":
|
||||||
|
out.Values[i] = ec._Meta_isMagicLoginEnabled(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -4223,6 +4377,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "magicLogin":
|
||||||
|
out.Values[i] = ec._Mutation_magicLogin(ctx, field)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "logout":
|
case "logout":
|
||||||
out.Values[i] = ec._Mutation_logout(ctx, field)
|
out.Values[i] = ec._Mutation_logout(ctx, field)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
|
@ -4800,6 +4959,11 @@ func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋ
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNMagicLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMagicLoginInput(ctx context.Context, v interface{}) (model.MagicLoginInput, error) {
|
||||||
|
res, err := ec.unmarshalInputMagicLoginInput(ctx, v)
|
||||||
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNMeta2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMeta(ctx context.Context, sel ast.SelectionSet, v model.Meta) graphql.Marshaler {
|
func (ec *executionContext) marshalNMeta2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐMeta(ctx context.Context, sel ast.SelectionSet, v model.Meta) graphql.Marshaler {
|
||||||
return ec._Meta(ctx, sel, &v)
|
return ec._Meta(ctx, sel, &v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,11 @@ type LoginInput struct {
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MagicLoginInput struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
IsGoogleLoginEnabled bool `json:"isGoogleLoginEnabled"`
|
IsGoogleLoginEnabled bool `json:"isGoogleLoginEnabled"`
|
||||||
|
@ -45,6 +50,7 @@ type Meta struct {
|
||||||
IsGithubLoginEnabled bool `json:"isGithubLoginEnabled"`
|
IsGithubLoginEnabled bool `json:"isGithubLoginEnabled"`
|
||||||
IsEmailVerificationEnabled bool `json:"isEmailVerificationEnabled"`
|
IsEmailVerificationEnabled bool `json:"isEmailVerificationEnabled"`
|
||||||
IsBasicAuthenticationEnabled bool `json:"isBasicAuthenticationEnabled"`
|
IsBasicAuthenticationEnabled bool `json:"isBasicAuthenticationEnabled"`
|
||||||
|
IsMagicLoginEnabled bool `json:"isMagicLoginEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResendVerifyEmailInput struct {
|
type ResendVerifyEmailInput struct {
|
||||||
|
|
|
@ -13,6 +13,7 @@ type Meta {
|
||||||
isGithubLoginEnabled: Boolean!
|
isGithubLoginEnabled: Boolean!
|
||||||
isEmailVerificationEnabled: Boolean!
|
isEmailVerificationEnabled: Boolean!
|
||||||
isBasicAuthenticationEnabled: Boolean!
|
isBasicAuthenticationEnabled: Boolean!
|
||||||
|
isMagicLoginEnabled: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
@ -112,9 +113,15 @@ input DeleteUserInput {
|
||||||
email: String!
|
email: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input MagicLoginInput {
|
||||||
|
email: String!
|
||||||
|
roles: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
signup(params: SignUpInput!): AuthResponse!
|
signup(params: SignUpInput!): AuthResponse!
|
||||||
login(params: LoginInput!): AuthResponse!
|
login(params: LoginInput!): AuthResponse!
|
||||||
|
magicLogin(params: MagicLoginInput!): Response!
|
||||||
logout: Response!
|
logout: Response!
|
||||||
updateProfile(params: UpdateProfileInput!): Response!
|
updateProfile(params: UpdateProfileInput!): Response!
|
||||||
adminUpdateUser(params: AdminUpdateUserInput!): User!
|
adminUpdateUser(params: AdminUpdateUserInput!): User!
|
||||||
|
|
|
@ -19,6 +19,10 @@ func (r *mutationResolver) Login(ctx context.Context, params model.LoginInput) (
|
||||||
return resolvers.Login(ctx, params)
|
return resolvers.Login(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) {
|
||||||
|
return resolvers.MagicLogin(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) Logout(ctx context.Context) (*model.Response, error) {
|
func (r *mutationResolver) Logout(ctx context.Context) (*model.Response, error) {
|
||||||
return resolvers.Logout(ctx)
|
return resolvers.Logout(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||||
|
|
||||||
signupMethod := existingUser.SignupMethod
|
signupMethod := existingUser.SignupMethod
|
||||||
if !strings.Contains(signupMethod, provider) {
|
if !strings.Contains(signupMethod, provider) {
|
||||||
signupMethod = signupMethod + "," + enum.Github.String()
|
signupMethod = signupMethod + "," + provider
|
||||||
}
|
}
|
||||||
user.SignupMethod = signupMethod
|
user.SignupMethod = signupMethod
|
||||||
user.Password = existingUser.Password
|
user.Password = existingUser.Password
|
||||||
|
|
|
@ -46,7 +46,9 @@ func VerifyEmailHandler() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// update email_verified_at in users table
|
// update email_verified_at in users table
|
||||||
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID)
|
if user.EmailVerifiedAt <= 0 {
|
||||||
|
db.Mgr.UpdateVerificationTime(time.Now().Unix(), user.ID)
|
||||||
|
}
|
||||||
// delete from verification table
|
// delete from verification table
|
||||||
db.Mgr.DeleteToken(claim.Email)
|
db.Mgr.DeleteToken(claim.Email)
|
||||||
|
|
||||||
|
|
122
server/resolvers/magicLogin.go
Normal file
122
server/resolvers/magicLogin.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
|
"github.com/authorizerdev/authorizer/server/enum"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/authorizerdev/authorizer/server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MagicLogin(ctx context.Context, params model.MagicLoginInput) (*model.Response, error) {
|
||||||
|
var res *model.Response
|
||||||
|
|
||||||
|
if constants.DISABLE_MAGIC_LOGIN == "true" {
|
||||||
|
return res, fmt.Errorf(`magic link login is disabled for this instance`)
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Email = strings.ToLower(params.Email)
|
||||||
|
|
||||||
|
if !utils.IsValidEmail(params.Email) {
|
||||||
|
return res, fmt.Errorf(`invalid email address`)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRoles := []string{}
|
||||||
|
|
||||||
|
user := db.User{
|
||||||
|
Email: params.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
// find user with email
|
||||||
|
existingUser, err := db.Mgr.GetUserByEmail(params.Email)
|
||||||
|
if err != nil {
|
||||||
|
user.SignupMethod = enum.MagicLink.String()
|
||||||
|
// define roles for new user
|
||||||
|
if len(params.Roles) > 0 {
|
||||||
|
// check if roles exists
|
||||||
|
if !utils.IsValidRoles(constants.ROLES, params.Roles) {
|
||||||
|
return res, fmt.Errorf(`invalid roles`)
|
||||||
|
} else {
|
||||||
|
inputRoles = params.Roles
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inputRoles = constants.DEFAULT_ROLES
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Roles = strings.Join(inputRoles, ",")
|
||||||
|
} else {
|
||||||
|
user = existingUser
|
||||||
|
// There multiple scenarios with roles here in magic link login
|
||||||
|
// 1. user has access to protected roles + roles and trying to login
|
||||||
|
// 2. user has not signed up for one of the available role but trying to signup.
|
||||||
|
// Need to modify roles in this case
|
||||||
|
|
||||||
|
// find the unassigned roles
|
||||||
|
existingRoles := strings.Split(existingUser.Roles, ",")
|
||||||
|
unasignedRoles := []string{}
|
||||||
|
for _, ir := range inputRoles {
|
||||||
|
if !utils.StringSliceContains(existingRoles, ir) {
|
||||||
|
unasignedRoles = append(unasignedRoles, ir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unasignedRoles) > 0 {
|
||||||
|
// check if it contains protected unassigned role
|
||||||
|
hasProtectedRole := false
|
||||||
|
for _, ur := range unasignedRoles {
|
||||||
|
if utils.StringSliceContains(constants.PROTECTED_ROLES, ur) {
|
||||||
|
hasProtectedRole = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasProtectedRole {
|
||||||
|
return res, fmt.Errorf(`invalid roles`)
|
||||||
|
} else {
|
||||||
|
user.Roles = existingUser.Roles + "," + strings.Join(unasignedRoles, ",")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user.Roles = existingUser.Roles
|
||||||
|
}
|
||||||
|
|
||||||
|
signupMethod := existingUser.SignupMethod
|
||||||
|
if !strings.Contains(signupMethod, enum.MagicLink.String()) {
|
||||||
|
signupMethod = signupMethod + "," + enum.MagicLink.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
user.SignupMethod = signupMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _ = db.Mgr.SaveUser(user)
|
||||||
|
|
||||||
|
if constants.DISABLE_EMAIL_VERIFICATION != "true" {
|
||||||
|
// insert verification request
|
||||||
|
verificationType := enum.MagicLink.String()
|
||||||
|
token, err := utils.CreateVerificationToken(params.Email, verificationType)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`Error generating token`, err)
|
||||||
|
}
|
||||||
|
db.Mgr.AddVerification(db.VerificationRequest{
|
||||||
|
Token: token,
|
||||||
|
Identifier: verificationType,
|
||||||
|
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||||
|
Email: params.Email,
|
||||||
|
})
|
||||||
|
|
||||||
|
// exec it as go routin so that we can reduce the api latency
|
||||||
|
go func() {
|
||||||
|
utils.SendVerificationMail(params.Email, token)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &model.Response{
|
||||||
|
Message: `Verification request has been sent. Please check your inbox!`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ func SendVerificationMail(toEmail, token string) error {
|
||||||
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
|
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
|
||||||
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
|
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
|
||||||
<p>Hey there 👋</p>
|
<p>Hey there 👋</p>
|
||||||
<p>We received a request to sign-up for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
|
<p>We received a request to sign-up / login for <b>%s</b>. If this is correct, please confirm your email address by clicking the button below.</p> <br/>
|
||||||
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a>
|
<a href="%s" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Confirm Email</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -16,5 +16,6 @@ func GetMetaInfo() model.Meta {
|
||||||
IsTwitterLoginEnabled: constants.TWITTER_CLIENT_ID != "" && constants.TWITTER_CLIENT_SECRET != "",
|
IsTwitterLoginEnabled: constants.TWITTER_CLIENT_ID != "" && constants.TWITTER_CLIENT_SECRET != "",
|
||||||
IsBasicAuthenticationEnabled: constants.DISABLE_BASIC_AUTHENTICATION != "true",
|
IsBasicAuthenticationEnabled: constants.DISABLE_BASIC_AUTHENTICATION != "true",
|
||||||
IsEmailVerificationEnabled: constants.DISABLE_EMAIL_VERIFICATION != "true",
|
IsEmailVerificationEnabled: constants.DISABLE_EMAIL_VERIFICATION != "true",
|
||||||
|
IsMagicLoginEnabled: constants.DISABLE_MAGIC_LOGIN != "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user