Fix #292: Users can now update their email address
This commit is contained in:
parent
0a93aec8c9
commit
3bec27ded3
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
|
|
||||||
|
from allauth.account import models as allauth_models
|
||||||
from rest_auth.serializers import PasswordResetSerializer as PRS
|
from rest_auth.serializers import PasswordResetSerializer as PRS
|
||||||
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -288,3 +289,29 @@ class LoginSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def save(self, request):
|
def save(self, request):
|
||||||
return auth.login(request, self.validated_data)
|
return auth.login(request, self.validated_data)
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangeEmailSerializer(serializers.Serializer):
|
||||||
|
password = serializers.CharField()
|
||||||
|
email = serializers.EmailField()
|
||||||
|
|
||||||
|
def validate_password(self, value):
|
||||||
|
if not self.instance.check_password(value):
|
||||||
|
raise serializers.ValidationError("Invalid password")
|
||||||
|
|
||||||
|
def validate_email(self, value):
|
||||||
|
if (
|
||||||
|
allauth_models.EmailAddress.objects.filter(email__iexact=value)
|
||||||
|
.exclude(user=self.context["user"])
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError("This email address is already in use")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def save(self, request):
|
||||||
|
current, _ = allauth_models.EmailAddress.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
email=request.user.email,
|
||||||
|
defaults={"verified": False, "primary": True},
|
||||||
|
)
|
||||||
|
current.change(request, self.validated_data["email"], confirm=True)
|
||||||
|
|
|
@ -111,6 +111,22 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
||||||
data = {"subsonic_api_token": self.request.user.subsonic_api_token}
|
data = {"subsonic_api_token": self.request.user.subsonic_api_token}
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
@action(
|
||||||
|
methods=["post"],
|
||||||
|
required_scope="security",
|
||||||
|
url_path="change-email",
|
||||||
|
detail=False,
|
||||||
|
)
|
||||||
|
def change_email(self, request, *args, **kwargs):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return Response(status=403)
|
||||||
|
serializer = serializers.UserChangeEmailSerializer(
|
||||||
|
request.user, data=request.data, context={"user": request.user}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save(request)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
if not self.request.user.username == kwargs.get("username"):
|
if not self.request.user.username == kwargs.get("username"):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
|
|
|
@ -568,3 +568,26 @@ def test_update_settings(logged_in_api_client, factories):
|
||||||
logged_in_api_client.user.refresh_from_db()
|
logged_in_api_client.user.refresh_from_db()
|
||||||
|
|
||||||
assert logged_in_api_client.user.settings == {"foo": "bar", "theme": "dark"}
|
assert logged_in_api_client.user.settings == {"foo": "bar", "theme": "dark"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_change_email_requires_valid_password(logged_in_api_client):
|
||||||
|
url = reverse("api:v1:users:users-change-email")
|
||||||
|
payload = {"password": "invalid", "email": "test@new.email"}
|
||||||
|
response = logged_in_api_client.post(url, payload)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_change_email(logged_in_api_client, mocker, mailoutbox):
|
||||||
|
user = logged_in_api_client.user
|
||||||
|
user.set_password("mypassword")
|
||||||
|
url = reverse("api:v1:users:users-change-email")
|
||||||
|
payload = {"password": "mypassword", "email": "test@new.email"}
|
||||||
|
response = logged_in_api_client.post(url, payload)
|
||||||
|
|
||||||
|
address = user.emailaddress_set.latest("id")
|
||||||
|
|
||||||
|
assert address.email == payload["email"]
|
||||||
|
assert address.verified is False
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert len(mailoutbox) == 1
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Users can now update their email address (#292)
|
|
@ -310,6 +310,53 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "./api/definitions.yml#/Me"
|
$ref: "./api/definitions.yml#/Me"
|
||||||
|
delete:
|
||||||
|
summary: Delete the user account performing the request
|
||||||
|
tags:
|
||||||
|
- "Auth and security"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
confirm:
|
||||||
|
type: "boolean"
|
||||||
|
description: "Must be set to true, to avoid accidental deletion"
|
||||||
|
password:
|
||||||
|
type: "string"
|
||||||
|
description: "The current password of the account"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "./api/definitions.yml#/Me"
|
||||||
|
/api/v1/users/users/change-email/:
|
||||||
|
post:
|
||||||
|
summary: Update the email address associated with a user account
|
||||||
|
tags:
|
||||||
|
- "Auth and security"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: "string"
|
||||||
|
format: "email"
|
||||||
|
password:
|
||||||
|
type: "string"
|
||||||
|
description: "The current password of the account"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "./api/definitions.yml#/Me"
|
||||||
|
|
||||||
/api/v1/rate-limit/:
|
/api/v1/rate-limit/:
|
||||||
get:
|
get:
|
||||||
|
|
|
@ -270,6 +270,38 @@
|
||||||
<translate translate-context="Content/Settings/Button.Label">Manage plugins</translate>
|
<translate translate-context="Content/Settings/Button.Label">Manage plugins</translate>
|
||||||
</router-link>
|
</router-link>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="ui text container">
|
||||||
|
<div class="ui hidden divider"></div>
|
||||||
|
<h2 class="ui header">
|
||||||
|
<i class="comment icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
<translate translate-context="*/*/Button.Label">Change my email address</translate>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<translate translate-context="Content/Settings/Paragraph'">Change the email address associated with your account. We will send a confirmation to the new address.</translate>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<translate :translate-params="{email: $store.state.auth.profile.email}" translate-context="Content/Settings/Paragraph'">Your current email address is %{ email }.</translate>
|
||||||
|
</p>
|
||||||
|
<form class="ui form" @submit.prevent="changeEmail">
|
||||||
|
<div v-if="changeEmailErrors.length > 0" role="alert" class="ui negative message">
|
||||||
|
<h4 class="header"><translate translate-context="Content/Settings/Error message.Title">We cannot change your email address</translate></h4>
|
||||||
|
<ul class="list">
|
||||||
|
<li v-for="error in changeEmailErrors">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="new-email"><translate translate-context="*/*/*">New email</translate></label>
|
||||||
|
<input id="new-email" required v-model="newEmail" type="email" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="current-password-field-email"><translate translate-context="*/*/*">Password</translate></label>
|
||||||
|
<password-input field-id="current-password-field-email" required v-model="emailPassword" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="ui button"><translate translate-context="*/*/*">Update</translate></button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<section class="ui text container">
|
<section class="ui text container">
|
||||||
<div class="ui hidden divider"></div>
|
<div class="ui hidden divider"></div>
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
|
@ -339,6 +371,10 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isLoadingAvatar: false,
|
isLoadingAvatar: false,
|
||||||
isDeletingAccount: false,
|
isDeletingAccount: false,
|
||||||
|
changeEmailErrors: [],
|
||||||
|
isChangingEmail: false,
|
||||||
|
newEmail: null,
|
||||||
|
emailPassword: null,
|
||||||
accountDeleteErrors: [],
|
accountDeleteErrors: [],
|
||||||
avatarErrors: [],
|
avatarErrors: [],
|
||||||
apps: [],
|
apps: [],
|
||||||
|
@ -519,6 +555,33 @@ export default {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
changeEmail() {
|
||||||
|
this.isChangingEmail = true
|
||||||
|
this.changeEmailErrors = []
|
||||||
|
let self = this
|
||||||
|
let payload = {
|
||||||
|
password: this.emailPassword,
|
||||||
|
email: this.newEmail,
|
||||||
|
}
|
||||||
|
axios.post(`users/users/change-email/`, payload)
|
||||||
|
.then(
|
||||||
|
response => {
|
||||||
|
self.isChangingEmail = false
|
||||||
|
self.newEmail = null
|
||||||
|
self.emailPassword = null
|
||||||
|
let msg = self.$pgettext('*/Auth/Message', 'Your email has been changed, please check your inbox for our confirmation message.')
|
||||||
|
self.$store.commit('ui/addMessage', {
|
||||||
|
content: msg,
|
||||||
|
date: new Date()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
self.isChangingEmail = false
|
||||||
|
self.changeEmailErrors = error.backendErrors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
labels() {
|
labels() {
|
||||||
|
|
Loading…
Reference in New Issue