Skip to content

Commit

Permalink
Merge pull request #4656 from mirumee/4287/Logged-in_user_password_ch…
Browse files Browse the repository at this point in the history
…ange

Add mutation to change the authenticated user's password
  • Loading branch information
maarcingebala committed Aug 21, 2019
2 parents 1f4c0e4 + 0de9ed4 commit 246c788
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Refactoring of password recovery system - #4617 by @fowczarek
- Added capability to filter attributes per global ID - #4640 by @NyanKiyoshi.
- Added capability to search product types by value (through the name) - #4647 by @NyanKiyoshi.
- Add mutation to change the authenticated user's password - #4656 by @fowczarek

## 2.8.0

Expand Down
1 change: 1 addition & 0 deletions saleor/account/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CustomerEvents:
ACCOUNT_CREATED = "account_created"
PASSWORD_RESET_LINK_SENT = "password_reset_link_sent"
PASSWORD_RESET = "password_reset"
PASSWORD_CHANGED = "password_changed"

# Order related events
PLACED_ORDER = "placed_order" # created an order
Expand Down
4 changes: 4 additions & 0 deletions saleor/account/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def customer_password_reset_event(*, user: UserType) -> Optional[CustomerEvent]:
return CustomerEvent.objects.create(user=user, type=CustomerEvents.PASSWORD_RESET)


def customer_password_changed_event(*, user: UserType) -> Optional[CustomerEvent]:
return CustomerEvent.objects.create(user=user, type=CustomerEvents.PASSWORD_CHANGED)


def customer_placed_order_event(
*, user: UserType, order: Order
) -> Optional[CustomerEvent]:
Expand Down
35 changes: 35 additions & 0 deletions saleor/graphql/account/mutations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ def perform_mutation(cls, _root, info, **data):
return RequestPasswordReset()


class PasswordChange(BaseMutation):
user = graphene.Field(User, description="A user instance with a new password.")

class Arguments:
old_password = graphene.String(
required=True, description="Current user password."
)
new_password = graphene.String(required=True, description="New user password.")

class Meta:
description = "Change the password of the logged in user."

@classmethod
def check_permissions(cls, user):
return user.is_authenticated

@classmethod
def perform_mutation(cls, _root, info, **data):
user = info.context.user
old_password = data["old_password"]
new_password = data["new_password"]

if not user.check_password(old_password):
raise ValidationError({"old_password": "Old password isn't valid."})
try:
password_validation.validate_password(new_password, user)
except ValidationError as error:
raise ValidationError({"new_password": error.messages})

user.set_password(new_password)
user.save(update_fields=["password"])
account_events.customer_password_changed_event(user=user)
return PasswordChange(user=user)


class BaseAddressUpdate(ModelMutation):
"""Base mutation for address update used by staff and account."""

Expand Down
2 changes: 2 additions & 0 deletions saleor/graphql/account/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
AccountUpdate,
)
from .mutations.base import (
PasswordChange,
RequestPasswordReset,
SetPassword,
UserClearStoredMeta,
Expand Down Expand Up @@ -118,6 +119,7 @@ class AccountMutations(graphene.ObjectType):
# Base mutations
request_password_reset = RequestPasswordReset.Field()
set_password = SetPassword.Field()
password_change = PasswordChange.Field()

user_update_metadata = UserUpdateMeta.Field()
user_clear_stored_metadata = UserClearStoredMeta.Field()
Expand Down
6 changes: 6 additions & 0 deletions saleor/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,7 @@ type Mutations {
checkoutClearPrivateMetadata(id: ID!, input: MetaPath!): CheckoutClearStoredPrivateMeta
requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset
setPassword(token: String!, email: String!, password: String!): SetPassword
passwordChange(newPassword: String!, oldPassword: String!): PasswordChange
userUpdateMetadata(id: ID!, input: MetaInput!): UserUpdateMeta
userClearStoredMetadata(id: ID!, input: MetaPath!): UserClearStoredMeta
accountAddressCreate(input: AddressInput!, type: AddressTypeEnum): AccountAddressCreate
Expand Down Expand Up @@ -2342,6 +2343,11 @@ type PageUpdate {
page: Page
}

type PasswordChange {
errors: [Error!]
user: User
}

type PasswordReset {
errors: [Error!]
}
Expand Down
72 changes: 72 additions & 0 deletions tests/api/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,78 @@ def test_password_reset_email_non_existing_user(
send_password_reset_mock.assert_not_called()


CHANGE_PASSWORD_MUTATION = """
mutation PasswordChange($oldPassword: String!, $newPassword: String!) {
passwordChange(oldPassword: $oldPassword, newPassword: $newPassword) {
errors {
field
message
}
user {
email
}
}
}
"""


def test_password_change(user_api_client):
customer_user = user_api_client.user
new_password = "spanish-inquisition"

variables = {"oldPassword": "password", "newPassword": new_password}
response = user_api_client.post_graphql(CHANGE_PASSWORD_MUTATION, variables)
content = get_graphql_content(response)
data = content["data"]["passwordChange"]
assert not data["errors"]
assert data["user"]["email"] == customer_user.email

customer_user.refresh_from_db()
assert customer_user.check_password(new_password)

password_change_event = account_events.CustomerEvent.objects.get()
assert password_change_event.type == account_events.CustomerEvents.PASSWORD_CHANGED
assert password_change_event.user == customer_user


def test_password_change_incorrect_old_password(user_api_client):
customer_user = user_api_client.user
variables = {"oldPassword": "incorrect", "newPassword": ""}
response = user_api_client.post_graphql(CHANGE_PASSWORD_MUTATION, variables)
content = get_graphql_content(response)
data = content["data"]["passwordChange"]
customer_user.refresh_from_db()
assert customer_user.check_password("password")
assert data["errors"]
assert data["errors"][0]["field"] == "oldPassword"


def test_password_change_invalid_new_password(user_api_client, settings):
settings.AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 5},
},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

customer_user = user_api_client.user
variables = {"oldPassword": "password", "newPassword": "1234"}
response = user_api_client.post_graphql(CHANGE_PASSWORD_MUTATION, variables)
content = get_graphql_content(response)
errors = content["data"]["passwordChange"]["errors"]
customer_user.refresh_from_db()
assert customer_user.check_password("password")
assert len(errors) == 2
assert errors[1]["field"] == "newPassword"
assert (
errors[0]["message"]
== "This password is too short. It must contain at least 5 characters."
)
assert errors[1]["field"] == "newPassword"
assert errors[1]["message"] == "This password is entirely numeric."


def test_create_address_mutation(
staff_api_client, customer_user, permission_manage_users
):
Expand Down

0 comments on commit 246c788

Please sign in to comment.