diff --git a/server/constants/webhook_event.go b/server/constants/webhook_event.go index 77b2361..35f7e1b 100644 --- a/server/constants/webhook_event.go +++ b/server/constants/webhook_event.go @@ -15,4 +15,6 @@ const ( UserAccessEnabledWebhookEvent = `user.access_enabled` // UserDeletedWebhookEvent name for user deleted event UserDeletedWebhookEvent = `user.deleted` + // UserDeactivatedWebhookEvent name for user deactivated event + UserDeactivatedWebhookEvent = `user.deactivated` ) diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index f323e42..2582a6b 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -175,6 +175,7 @@ type ComplexityRoot struct { AdminLogin func(childComplexity int, params model.AdminLoginInput) int AdminLogout func(childComplexity int) int AdminSignup func(childComplexity int, params model.AdminSignupInput) int + DeactivateAccount func(childComplexity int) int DeleteEmailTemplate func(childComplexity int, params model.DeleteEmailTemplateRequest) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int DeleteWebhook func(childComplexity int, params model.WebhookRequest) int @@ -347,6 +348,7 @@ type MutationResolver interface { Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) + DeactivateAccount(ctx context.Context) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) @@ -1159,6 +1161,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AdminSignup(childComplexity, args["params"].(model.AdminSignupInput)), true + case "Mutation.deactivate_account": + if e.complexity.Mutation.DeactivateAccount == nil { + break + } + + return e.complexity.Mutation.DeactivateAccount(childComplexity), true + case "Mutation._delete_email_template": if e.complexity.Mutation.DeleteEmailTemplate == nil { break @@ -2789,6 +2798,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + deactivate_account: Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! @@ -8745,6 +8755,54 @@ func (ec *executionContext) fieldContext_Mutation_resend_otp(ctx context.Context return fc, nil } +func (ec *executionContext) _Mutation_deactivate_account(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deactivate_account(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeactivateAccount(rctx) + }) + 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) fieldContext_Mutation_deactivate_account(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "message": + return ec.fieldContext_Response_message(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Response", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation__delete_user(ctx, field) if err != nil { @@ -18942,6 +19000,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_resend_otp(ctx, field) }) + if out.Values[i] == graphql.Null { + invalids++ + } + case "deactivate_account": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deactivate_account(ctx, field) + }) + if out.Values[i] == graphql.Null { invalids++ } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 1c7de22..b9c9600 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -587,6 +587,7 @@ type Mutation { revoke(params: OAuthRevokeInput!): Response! verify_otp(params: VerifyOTPRequest!): AuthResponse! resend_otp(params: ResendOTPRequest!): Response! + deactivate_account: Response! # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index eecb6b2..89c44a2 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -5,6 +5,7 @@ package graph import ( "context" + "fmt" "github.com/authorizerdev/authorizer/server/graph/generated" "github.com/authorizerdev/authorizer/server/graph/model" @@ -81,6 +82,11 @@ func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTP return resolvers.ResendOTPResolver(ctx, params) } +// DeactivateAccount is the resolver for the deactivate_account field. +func (r *mutationResolver) DeactivateAccount(ctx context.Context) (*model.Response, error) { + panic(fmt.Errorf("not implemented: DeactivateAccount - deactivate_account")) +} + // DeleteUser is the resolver for the _delete_user field. func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { return resolvers.DeleteUserResolver(ctx, params) diff --git a/server/resolvers/deactivate_account.go b/server/resolvers/deactivate_account.go new file mode 100644 index 0000000..6248792 --- /dev/null +++ b/server/resolvers/deactivate_account.go @@ -0,0 +1,58 @@ +package resolvers + +import ( + "context" + "time" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/memorystore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/authorizerdev/authorizer/server/utils" + log "github.com/sirupsen/logrus" +) + +// DeactivateAccountResolver is the resolver for the deactivate_account field. +func DeactivateAccountResolver(ctx context.Context) (*model.Response, error) { + var res *model.Response + gc, err := utils.GinContextFromContext(ctx) + if err != nil { + log.Debug("Failed to get GinContext: ", err) + return res, err + } + accessToken, err := token.GetAccessToken(gc) + if err != nil { + log.Debug("Failed to get access token: ", err) + return res, err + } + claims, err := token.ValidateAccessToken(gc, accessToken) + if err != nil { + log.Debug("Failed to validate access token: ", err) + return res, err + } + userID := claims["sub"].(string) + log := log.WithFields(log.Fields{ + "user_id": userID, + }) + user, err := db.Provider.GetUserByID(ctx, userID) + if err != nil { + log.Debug("Failed to get user by id: ", err) + return res, err + } + now := time.Now().Unix() + user.RevokedTimestamp = &now + user, err = db.Provider.UpdateUser(ctx, user) + if err != nil { + log.Debug("Failed to update user: ", err) + return res, err + } + go func() { + memorystore.Provider.DeleteAllUserSessions(user.ID) + utils.RegisterEvent(ctx, constants.UserAccessRevokedWebhookEvent, "", user) + }() + res = &model.Response{ + Message: `user account deactivated successfully`, + } + return res, nil +} diff --git a/server/test/deactivate_account_test.go b/server/test/deactivate_account_test.go new file mode 100644 index 0000000..eba8e49 --- /dev/null +++ b/server/test/deactivate_account_test.go @@ -0,0 +1,45 @@ +package test + +import ( + "context" + "testing" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func deactivateAccountTests(t *testing.T, s TestSetup) { + t.Helper() + t.Run(`should deactiavte the user account with access token only`, func(t *testing.T) { + req, ctx := createContext(s) + email := "deactiavte_account." + s.TestInfo.Email + + resolvers.SignupResolver(ctx, model.SignUpInput{ + Email: email, + Password: s.TestInfo.Password, + ConfirmPassword: s.TestInfo.Password, + }) + _, err := resolvers.DeactivateAccountResolver(ctx) + assert.NotNil(t, err, "unauthorized") + verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup) + assert.NoError(t, err) + assert.NotNil(t, verificationRequest) + verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{ + Token: verificationRequest.Token, + }) + assert.NoError(t, err) + assert.NotNil(t, verifyRes) + s.GinContext.Request.Header.Set("Authorization", "Bearer "+*verifyRes.AccessToken) + ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext) + _, err = resolvers.DeactivateAccountResolver(ctx) + assert.NoError(t, err) + s.GinContext.Request.Header.Set("Authorization", "") + assert.Nil(t, err) + _, err = resolvers.ProfileResolver(ctx) + assert.NotNil(t, err, "unauthorized") + cleanData(email) + }) +} diff --git a/server/test/integration_test.go b/server/test/integration_test.go index 3d4bb3d..a11376e 100644 --- a/server/test/integration_test.go +++ b/server/test/integration_test.go @@ -143,6 +143,7 @@ func TestResolvers(t *testing.T) { verifyOTPTest(t, s) resendOTPTest(t, s) validateSessionTests(t, s) + deactivateAccountTests(t, s) updateAllUsersTest(t, s) webhookLogsTest(t, s) // get logs after above resolver tests are done