Merge pull request #399 from authorizerdev/feat/deativate-account

Add api to deactivate user account
This commit is contained in:
Lakhan Samani 2023-10-11 00:22:15 +05:30 committed by GitHub
commit 7ced811e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 2 deletions

View File

@ -222,6 +222,7 @@ export const webhookEventNames = {
'User deleted': 'user.deleted',
'User access enabled': 'user.access_enabled',
'User access revoked': 'user.access_revoked',
'User deactivated': 'user.deactivated',
};
export const emailTemplateEventNames = {

View File

@ -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`
)

View File

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

View File

@ -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!

View File

@ -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)

View File

@ -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.UserDeactivatedWebhookEvent, "", user)
}()
res = &model.Response{
Message: `user account deactivated successfully`,
}
return res, nil
}

View File

@ -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)
})
}

View File

@ -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

View File

@ -103,7 +103,7 @@ func testSetup() TestSetup {
Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()),
Password: "Test@123",
WebhookEndpoint: "https://62f93101e05644803533cf36.mockapi.io/authorizer/webhook",
TestWebhookEventTypes: []string{constants.UserAccessEnabledWebhookEvent, constants.UserAccessRevokedWebhookEvent, constants.UserCreatedWebhookEvent, constants.UserDeletedWebhookEvent, constants.UserLoginWebhookEvent, constants.UserSignUpWebhookEvent},
TestWebhookEventTypes: []string{constants.UserAccessEnabledWebhookEvent, constants.UserAccessRevokedWebhookEvent, constants.UserCreatedWebhookEvent, constants.UserDeletedWebhookEvent, constants.UserLoginWebhookEvent, constants.UserSignUpWebhookEvent, constants.UserDeactivatedWebhookEvent},
TestEmailTemplateEventTypes: []string{constants.VerificationTypeBasicAuthSignup, constants.VerificationTypeForgotPassword, constants.VerificationTypeMagicLinkLogin, constants.VerificationTypeUpdateEmail},
}

View File

@ -4,7 +4,7 @@ import "github.com/authorizerdev/authorizer/server/constants"
// IsValidWebhookEventName to validate webhook event name
func IsValidWebhookEventName(eventName string) bool {
if eventName != constants.UserCreatedWebhookEvent && eventName != constants.UserLoginWebhookEvent && eventName != constants.UserSignUpWebhookEvent && eventName != constants.UserDeletedWebhookEvent && eventName != constants.UserAccessEnabledWebhookEvent && eventName != constants.UserAccessRevokedWebhookEvent {
if eventName != constants.UserCreatedWebhookEvent && eventName != constants.UserLoginWebhookEvent && eventName != constants.UserSignUpWebhookEvent && eventName != constants.UserDeletedWebhookEvent && eventName != constants.UserAccessEnabledWebhookEvent && eventName != constants.UserAccessRevokedWebhookEvent && eventName != constants.UserDeactivatedWebhookEvent {
return false
}