diff --git a/dashboard/src/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index df5db93..dcadcc3 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -53,3 +53,19 @@ export const InviteMembers = ` } } `; + +export const RevokeAccess = ` + mutation revokeAccess($param: UpdateAccessInput!) { + _revoke_access(param: $param) { + message + } + } +`; + +export const EnableAccess = ` + mutation revokeAccess($param: UpdateAccessInput!) { + _enable_access(param: $param) { + message + } + } +`; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index fe35528..a7d0142 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -81,6 +81,7 @@ export const UserDetailsQuery = ` signup_methods roles created_at + revoked_timestamp } } } diff --git a/dashboard/src/pages/Users.tsx b/dashboard/src/pages/Users.tsx index a231117..6da5c83 100644 --- a/dashboard/src/pages/Users.tsx +++ b/dashboard/src/pages/Users.tsx @@ -39,7 +39,7 @@ import { FaAngleDown, } from 'react-icons/fa'; import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries'; -import { UpdateUser } from '../graphql/mutation'; +import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation'; import EditUserModal from '../components/EditUserModal'; import DeleteUserModal from '../components/DeleteUserModal'; import InviteMembersModal from '../components/InviteMembersModal'; @@ -67,6 +67,12 @@ interface userDataTypes { signup_methods: string; roles: [string]; created_at: number; + revoked_timestamp: number; +} + +const enum updateAccessActions { + REVOKE = 'REVOKE', + ENABLE = 'ENABLE', } const getMaxPages = (pagination: paginationPropTypes) => { @@ -185,6 +191,66 @@ export default function Users() { updateUserList(); }; + const updateAccessHandler = async ( + id: string, + action: updateAccessActions + ) => { + switch (action) { + case updateAccessActions.ENABLE: + const enableAccessRes = await client + .mutation(EnableAccess, { + param: { + user_id: id, + }, + }) + .toPromise(); + if (enableAccessRes.error) { + toast({ + title: 'User access enable failed', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else { + toast({ + title: 'User access enabled successfully', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + updateUserList(); + break; + case updateAccessActions.REVOKE: + const revokeAccessRes = await client + .mutation(RevokeAccess, { + param: { + user_id: id, + }, + }) + .toPromise(); + if (revokeAccessRes.error) { + toast({ + title: 'User access revoke failed', + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else { + toast({ + title: 'User access revoked successfully', + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + updateUserList(); + break; + default: + break; + } + }; + return ( @@ -206,6 +272,7 @@ export default function Users() { Signup Methods Roles Verified + Access Actions @@ -214,7 +281,7 @@ export default function Users() { const { email_verified, created_at, ...rest }: any = user; return ( - {user.email} + {user.email} {dayjs(user.created_at * 1000).format('MMM DD, YYYY')} @@ -229,6 +296,15 @@ export default function Users() { {user.email_verified.toString()} + + + {user.revoked_timestamp ? 'Revoked' : 'Enabled'} + + @@ -258,6 +334,29 @@ export default function Users() { user={rest} updateUserList={updateUserList} /> + {user.revoked_timestamp ? ( + + updateAccessHandler( + user.id, + updateAccessActions.ENABLE + ) + } + > + Enable Access + + ) : ( + + updateAccessHandler( + user.id, + updateAccessActions.REVOKE + ) + } + > + Revoke Access + + )} diff --git a/server/db/models/user.go b/server/db/models/user.go index b4e9dfd..d07486a 100644 --- a/server/db/models/user.go +++ b/server/db/models/user.go @@ -27,6 +27,7 @@ type User struct { Roles string `json:"roles" bson:"roles"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"` CreatedAt int64 `json:"created_at" bson:"created_at"` + RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp"` } func (user *User) AsAPIUser() *model.User { @@ -35,6 +36,7 @@ func (user *User) AsAPIUser() *model.User { email := user.Email createdAt := user.CreatedAt updatedAt := user.UpdatedAt + revokedTimestamp := user.RevokedTimestamp return &model.User{ ID: user.ID, Email: user.Email, @@ -53,5 +55,6 @@ func (user *User) AsAPIUser() *model.User { Roles: strings.Split(user.Roles, ","), CreatedAt: &createdAt, UpdatedAt: &updatedAt, + RevokedTimestamp: revokedTimestamp, } } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 9d5300e..70b0e76 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -115,6 +115,7 @@ type ComplexityRoot struct { AdminLogout func(childComplexity int) int AdminSignup func(childComplexity int, params model.AdminSignupInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int + EnableAccess func(childComplexity int, param model.UpdateAccessInput) 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 @@ -123,6 +124,7 @@ type ComplexityRoot struct { ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int Revoke func(childComplexity int, params model.OAuthRevokeInput) int + RevokeAccess func(childComplexity int, param model.UpdateAccessInput) int Signup func(childComplexity int, params model.SignUpInput) int UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int @@ -167,6 +169,7 @@ type ComplexityRoot struct { PhoneNumberVerified func(childComplexity int) int Picture func(childComplexity int) int PreferredUsername func(childComplexity int) int + RevokedTimestamp func(childComplexity int) int Roles func(childComplexity int) int SignupMethods func(childComplexity int) int UpdatedAt func(childComplexity int) int @@ -217,6 +220,8 @@ type MutationResolver interface { 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) + RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) + EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -672,6 +677,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteUser(childComplexity, args["params"].(model.DeleteUserInput)), true + case "Mutation._enable_access": + if e.complexity.Mutation.EnableAccess == nil { + break + } + + args, err := ec.field_Mutation__enable_access_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.EnableAccess(childComplexity, args["param"].(model.UpdateAccessInput)), true + case "Mutation.forgot_password": if e.complexity.Mutation.ForgotPassword == nil { break @@ -763,6 +780,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true + case "Mutation._revoke_access": + if e.complexity.Mutation.RevokeAccess == nil { + break + } + + args, err := ec.field_Mutation__revoke_access_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RevokeAccess(childComplexity, args["param"].(model.UpdateAccessInput)), true + case "Mutation.signup": if e.complexity.Mutation.Signup == nil { break @@ -1032,6 +1061,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.PreferredUsername(childComplexity), true + case "User.revoked_timestamp": + if e.complexity.User.RevokedTimestamp == nil { + break + } + + return e.complexity.User.RevokedTimestamp(childComplexity), true + case "User.roles": if e.complexity.User.Roles == nil { break @@ -1260,6 +1296,7 @@ type User { roles: [String!]! created_at: Int64 updated_at: Int64 + revoked_timestamp: Int64 } type Users { @@ -1502,6 +1539,10 @@ input InviteMemberInput { redirect_uri: String } +input UpdateAccessInput { + user_id: String! +} + input ValidateJWTTokenInput { token_type: String! token: String! @@ -1527,6 +1568,8 @@ type Mutation { _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! _invite_members(params: InviteMemberInput!): Response! + _revoke_access(param: UpdateAccessInput!): Response! + _enable_access(param: UpdateAccessInput!): Response! } type Query { @@ -1593,6 +1636,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation__enable_access_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.UpdateAccessInput + if tmp, ok := rawArgs["param"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("param")) + arg0, err = ec.unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["param"] = arg0 + 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{}{} @@ -1608,6 +1666,21 @@ func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation__revoke_access_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.UpdateAccessInput + if tmp, ok := rawArgs["param"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("param")) + arg0, err = ec.unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["param"] = 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{}{} @@ -4397,6 +4470,90 @@ func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__revoke_access(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__revoke_access_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().RevokeAccess(rctx, args["param"].(model.UpdateAccessInput)) + }) + 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__enable_access(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__enable_access_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().EnableAccess(rctx, args["param"].(model.UpdateAccessInput)) + }) + 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 { @@ -5510,6 +5667,38 @@ func (ec *executionContext) _User_updated_at(ctx context.Context, field graphql. return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) } +func (ec *executionContext) _User_revoked_timestamp(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + 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.RevokedTimestamp, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int64) + fc.Result = res + return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) +} + func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7644,6 +7833,29 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i return it, nil } +func (ec *executionContext) unmarshalInputUpdateAccessInput(ctx context.Context, obj interface{}) (model.UpdateAccessInput, error) { + var it model.UpdateAccessInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "user_id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("user_id")) + it.UserID, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, obj interface{}) (model.UpdateEnvInput, error) { var it model.UpdateEnvInput asMap := map[string]interface{}{} @@ -8572,6 +8784,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_revoke_access": + out.Values[i] = ec._Mutation__revoke_access(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "_enable_access": + out.Values[i] = ec._Mutation__enable_access(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8854,6 +9076,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._User_created_at(ctx, field, obj) case "updated_at": out.Values[i] = ec._User_updated_at(ctx, field, obj) + case "revoked_timestamp": + out.Values[i] = ec._User_revoked_timestamp(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9466,6 +9690,11 @@ func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel return ret } +func (ec *executionContext) unmarshalNUpdateAccessInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateAccessInput(ctx context.Context, v interface{}) (model.UpdateAccessInput, error) { + res, err := ec.unmarshalInputUpdateAccessInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateEnvInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUpdateEnvInput(ctx context.Context, v interface{}) (model.UpdateEnvInput, error) { res, err := ec.unmarshalInputUpdateEnvInput(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 ec260cf..ecb54c4 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -164,6 +164,10 @@ type SignUpInput struct { RedirectURI *string `json:"redirect_uri"` } +type UpdateAccessInput struct { + UserID string `json:"user_id"` +} + type UpdateEnvInput struct { AdminSecret *string `json:"ADMIN_SECRET"` CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` @@ -249,6 +253,7 @@ type User struct { Roles []string `json:"roles"` CreatedAt *int64 `json:"created_at"` UpdatedAt *int64 `json:"updated_at"` + RevokedTimestamp *int64 `json:"revoked_timestamp"` } type Users struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 65cdf67..92af52f 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -43,6 +43,7 @@ type User { roles: [String!]! created_at: Int64 updated_at: Int64 + revoked_timestamp: Int64 } type Users { @@ -285,6 +286,10 @@ input InviteMemberInput { redirect_uri: String } +input UpdateAccessInput { + user_id: String! +} + input ValidateJWTTokenInput { token_type: String! token: String! @@ -310,6 +315,8 @@ type Mutation { _admin_logout: Response! _update_env(params: UpdateEnvInput!): Response! _invite_members(params: InviteMemberInput!): Response! + _revoke_access(param: UpdateAccessInput!): Response! + _enable_access(param: UpdateAccessInput!): Response! } type Query { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 5b8501c..33f2055 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -79,6 +79,14 @@ func (r *mutationResolver) InviteMembers(ctx context.Context, params model.Invit return resolvers.InviteMembersResolver(ctx, params) } +func (r *mutationResolver) RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) { + return resolvers.RevokeAccessResolver(ctx, param) +} + +func (r *mutationResolver) EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) { + return resolvers.EnableAccessResolver(ctx, param) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.MetaResolver(ctx) } @@ -117,7 +125,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 0c6ffd5..c9563a6 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -95,9 +95,12 @@ func OAuthCallbackHandler() gin.HandlerFunc { user.EmailVerifiedAt = &now user, _ = db.Provider.AddUser(user) } else { + if user.RevokedTimestamp != nil { + c.JSON(400, gin.H{"error": "user access has been revoked"}) + } + // user exists in db, check if method was google // if not append google to existing signup method and save it - signupMethod := existingUser.SignupMethods if !strings.Contains(signupMethod, provider) { signupMethod = signupMethod + "," + provider diff --git a/server/resolvers/enable_access.go b/server/resolvers/enable_access.go new file mode 100644 index 0000000..647cada --- /dev/null +++ b/server/resolvers/enable_access.go @@ -0,0 +1,44 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// EnableAccessResolver is a resolver for enabling user access +func EnableAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, fmt.Errorf("unauthorized") + } + + user, err := db.Provider.GetUserByID(params.UserID) + if err != nil { + return res, err + } + + user.RevokedTimestamp = nil + + user, err = db.Provider.UpdateUser(user) + if err != nil { + log.Println("error updating user:", err) + return res, err + } + + res = &model.Response{ + Message: `user access enabled successfully`, + } + + return res, nil +} diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 355c77c..57f8158 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -35,6 +35,10 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return res, fmt.Errorf(`user with this email not found`) } + if user.RevokedTimestamp != nil { + return res, fmt.Errorf(`user access has been revoked`) + } + if !strings.Contains(user.SignupMethods, constants.SignupMethodBasicAuth) { return res, fmt.Errorf(`user has not signed up email & password`) } diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index b69ea91..1c9d0bc 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -70,6 +70,10 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu // 2. user has not signed up for one of the available role but trying to signup. // Need to modify roles in this case + if user.RevokedTimestamp != nil { + return res, fmt.Errorf(`user access has been revoked`) + } + // find the unassigned roles if len(params.Roles) <= 0 { inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles) diff --git a/server/resolvers/revoke_access.go b/server/resolvers/revoke_access.go new file mode 100644 index 0000000..a470be8 --- /dev/null +++ b/server/resolvers/revoke_access.go @@ -0,0 +1,49 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" +) + +// RevokeAccessResolver is a resolver for revoking user access +func RevokeAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.Response + if err != nil { + return res, err + } + + if !token.IsSuperAdmin(gc) { + return res, fmt.Errorf("unauthorized") + } + + user, err := db.Provider.GetUserByID(params.UserID) + if err != nil { + return res, err + } + + now := time.Now().Unix() + user.RevokedTimestamp = &now + + user, err = db.Provider.UpdateUser(user) + if err != nil { + log.Println("error updating user:", err) + return res, err + } + + go sessionstore.DeleteAllUserSession(fmt.Sprintf("%x", user.ID)) + + res = &model.Response{ + Message: `user access revoked successfully`, + } + + return res, nil +}