Merge branch '292-change-email' into 'develop'

Fix #292: Users can now update their email address

Closes #292

See merge request funkwhale/funkwhale!1191
environments/review-docs-devel-1399dq/deployments/6607
Agate 2020-08-02 17:48:36 +02:00
commit 36a6a0018e
6 zmienionych plików z 177 dodań i 0 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from django.contrib import auth
from allauth.account import models as allauth_models
from rest_auth.serializers import PasswordResetSerializer as PRS
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
from rest_framework import serializers
@ -288,3 +289,29 @@ class LoginSerializer(serializers.Serializer):
def save(self, request):
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)

Wyświetl plik

@ -111,6 +111,22 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
data = {"subsonic_api_token": self.request.user.subsonic_api_token}
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):
if not self.request.user.username == kwargs.get("username"):
return Response(status=403)

Wyświetl plik

@ -568,3 +568,26 @@ def test_update_settings(logged_in_api_client, factories):
logged_in_api_client.user.refresh_from_db()
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

Wyświetl plik

@ -0,0 +1 @@
Users can now update their email address (#292)

Wyświetl plik

@ -310,6 +310,53 @@ paths:
application/json:
schema:
$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/:
get:

Wyświetl plik

@ -270,6 +270,38 @@
<translate translate-context="Content/Settings/Button.Label">Manage plugins</translate>
</router-link>
</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">
<div class="ui hidden divider"></div>
<h2 class="ui header">
@ -339,6 +371,10 @@ export default {
isLoading: false,
isLoadingAvatar: false,
isDeletingAccount: false,
changeEmailErrors: [],
isChangingEmail: false,
newEmail: null,
emailPassword: null,
accountDeleteErrors: [],
avatarErrors: [],
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: {
labels() {