kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Resolve "An avatar for users"
rodzic
b411df4f29
commit
af270f4abd
|
@ -94,6 +94,7 @@ THIRD_PARTY_APPS = (
|
|||
"django_filters",
|
||||
"cacheops",
|
||||
"django_cleanup",
|
||||
"versatileimagefield",
|
||||
)
|
||||
|
||||
|
||||
|
@ -449,6 +450,7 @@ ACCOUNT_USERNAME_BLACKLIST = [
|
|||
"superuser",
|
||||
"staff",
|
||||
"service",
|
||||
"me",
|
||||
] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[])
|
||||
|
||||
EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from django.db import transaction
|
||||
|
@ -41,3 +45,22 @@ def set_query_parameter(url, **kwargs):
|
|||
new_query_string = urlencode(query_params, doseq=True)
|
||||
|
||||
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||
|
||||
|
||||
@deconstructible
|
||||
class ChunkedPath(object):
|
||||
def __init__(self, root, preserve_file_name=True):
|
||||
self.root = root
|
||||
self.preserve_file_name = preserve_file_name
|
||||
|
||||
def __call__(self, instance, filename):
|
||||
uid = str(uuid.uuid4())
|
||||
chunk_size = 2
|
||||
chunks = [uid[i : i + chunk_size] for i in range(0, len(uid), chunk_size)]
|
||||
if self.preserve_file_name:
|
||||
parts = chunks[:3] + [filename]
|
||||
else:
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
new_filename = "".join(chunks[3:]) + ".{}".format(ext)
|
||||
parts = chunks[:3] + [new_filename]
|
||||
return os.path.join(self.root, *parts)
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import mimetypes
|
||||
from os.path import splitext
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.images import get_image_dimensions
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
@deconstructible
|
||||
class ImageDimensionsValidator:
|
||||
"""
|
||||
ImageField dimensions validator.
|
||||
|
||||
from https://gist.github.com/emilio-rst/4f81ea2718736a6aaf9bdb64d5f2ea6c
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width=None,
|
||||
height=None,
|
||||
min_width=None,
|
||||
max_width=None,
|
||||
min_height=None,
|
||||
max_height=None,
|
||||
):
|
||||
"""
|
||||
Constructor
|
||||
|
||||
Args:
|
||||
width (int): exact width
|
||||
height (int): exact height
|
||||
min_width (int): minimum width
|
||||
min_height (int): minimum height
|
||||
max_width (int): maximum width
|
||||
max_height (int): maximum height
|
||||
"""
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.min_width = min_width
|
||||
self.max_width = max_width
|
||||
self.min_height = min_height
|
||||
self.max_height = max_height
|
||||
|
||||
def __call__(self, image):
|
||||
w, h = get_image_dimensions(image)
|
||||
|
||||
if self.width is not None and w != self.width:
|
||||
raise ValidationError(_("Width must be %dpx.") % (self.width,))
|
||||
|
||||
if self.height is not None and h != self.height:
|
||||
raise ValidationError(_("Height must be %dpx.") % (self.height,))
|
||||
|
||||
if self.min_width is not None and w < self.min_width:
|
||||
raise ValidationError(_("Minimum width must be %dpx.") % (self.min_width,))
|
||||
|
||||
if self.min_height is not None and h < self.min_height:
|
||||
raise ValidationError(
|
||||
_("Minimum height must be %dpx.") % (self.min_height,)
|
||||
)
|
||||
|
||||
if self.max_width is not None and w > self.max_width:
|
||||
raise ValidationError(_("Maximum width must be %dpx.") % (self.max_width,))
|
||||
|
||||
if self.max_height is not None and h > self.max_height:
|
||||
raise ValidationError(
|
||||
_("Maximum height must be %dpx.") % (self.max_height,)
|
||||
)
|
||||
|
||||
|
||||
@deconstructible
|
||||
class FileValidator(object):
|
||||
"""
|
||||
Taken from https://gist.github.com/jrosebr1/2140738
|
||||
Validator for files, checking the size, extension and mimetype.
|
||||
Initialization parameters:
|
||||
allowed_extensions: iterable with allowed file extensions
|
||||
ie. ('txt', 'doc')
|
||||
allowd_mimetypes: iterable with allowed mimetypes
|
||||
ie. ('image/png', )
|
||||
min_size: minimum number of bytes allowed
|
||||
ie. 100
|
||||
max_size: maximum number of bytes allowed
|
||||
ie. 24*1024*1024 for 24 MB
|
||||
Usage example::
|
||||
MyModel(models.Model):
|
||||
myfile = FileField(validators=FileValidator(max_size=24*1024*1024), ...)
|
||||
"""
|
||||
|
||||
extension_message = _(
|
||||
"Extension '%(extension)s' not allowed. Allowed extensions are: '%(allowed_extensions)s.'"
|
||||
)
|
||||
mime_message = _(
|
||||
"MIME type '%(mimetype)s' is not valid. Allowed types are: %(allowed_mimetypes)s."
|
||||
)
|
||||
min_size_message = _(
|
||||
"The current file %(size)s, which is too small. The minumum file size is %(allowed_size)s."
|
||||
)
|
||||
max_size_message = _(
|
||||
"The current file %(size)s, which is too large. The maximum file size is %(allowed_size)s."
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allowed_extensions = kwargs.pop("allowed_extensions", None)
|
||||
self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", None)
|
||||
self.min_size = kwargs.pop("min_size", 0)
|
||||
self.max_size = kwargs.pop("max_size", None)
|
||||
|
||||
def __call__(self, value):
|
||||
"""
|
||||
Check the extension, content type and file size.
|
||||
"""
|
||||
|
||||
# Check the extension
|
||||
ext = splitext(value.name)[1][1:].lower()
|
||||
if self.allowed_extensions and ext not in self.allowed_extensions:
|
||||
message = self.extension_message % {
|
||||
"extension": ext,
|
||||
"allowed_extensions": ", ".join(self.allowed_extensions),
|
||||
}
|
||||
|
||||
raise ValidationError(message)
|
||||
|
||||
# Check the content type
|
||||
mimetype = mimetypes.guess_type(value.name)[0]
|
||||
if self.allowed_mimetypes and mimetype not in self.allowed_mimetypes:
|
||||
message = self.mime_message % {
|
||||
"mimetype": mimetype,
|
||||
"allowed_mimetypes": ", ".join(self.allowed_mimetypes),
|
||||
}
|
||||
|
||||
raise ValidationError(message)
|
||||
|
||||
# Check the file size
|
||||
filesize = len(value)
|
||||
if self.max_size and filesize > self.max_size:
|
||||
message = self.max_size_message % {
|
||||
"size": filesizeformat(filesize),
|
||||
"allowed_size": filesizeformat(self.max_size),
|
||||
}
|
||||
|
||||
raise ValidationError(message)
|
||||
|
||||
elif filesize < self.min_size:
|
||||
message = self.min_size_message % {
|
||||
"size": filesizeformat(filesize),
|
||||
"allowed_size": filesizeformat(self.min_size),
|
||||
}
|
||||
|
||||
raise ValidationError(message)
|
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
|
@ -9,6 +8,7 @@ from django.db import models
|
|||
from django.utils import timezone
|
||||
|
||||
from funkwhale_api.common import session
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.music import utils as music_utils
|
||||
|
||||
TYPE_CHOICES = [
|
||||
|
@ -141,12 +141,7 @@ class Library(models.Model):
|
|||
)
|
||||
|
||||
|
||||
def get_file_path(instance, filename):
|
||||
uid = str(uuid.uuid4())
|
||||
chunk_size = 2
|
||||
chunks = [uid[i : i + chunk_size] for i in range(0, len(uid), chunk_size)]
|
||||
parts = chunks[:3] + [filename]
|
||||
return os.path.join("federation_cache", *parts)
|
||||
get_file_path = common_utils.ChunkedPath("federation_cache")
|
||||
|
||||
|
||||
class LibraryTrack(models.Model):
|
||||
|
|
|
@ -56,7 +56,10 @@ class UserAdmin(AuthUserAdmin):
|
|||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("username", "password", "privacy_level")}),
|
||||
(_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
|
||||
(
|
||||
_("Personal info"),
|
||||
{"fields": ("first_name", "last_name", "email", "avatar")},
|
||||
),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.0.6 on 2018-07-10 20:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import funkwhale_api.common.utils
|
||||
import funkwhale_api.common.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_auto_20180619_2024'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='avatar',
|
||||
field=models.ImageField(blank=True, max_length=150, null=True, upload_to=funkwhale_api.common.utils.ChunkedPath('users/avatars'), validators=[funkwhale_api.common.validators.ImageDimensionsValidator(max_height=400, max_width=400, min_height=50, min_width=50)]),
|
||||
),
|
||||
]
|
|
@ -16,7 +16,11 @@ from django.utils import timezone
|
|||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from versatileimagefield.fields import VersatileImageField
|
||||
|
||||
from funkwhale_api.common import fields, preferences
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.common import validators as common_validators
|
||||
|
||||
|
||||
def get_token():
|
||||
|
@ -39,6 +43,9 @@ PERMISSIONS_CONFIGURATION = {
|
|||
PERMISSIONS = sorted(PERMISSIONS_CONFIGURATION.keys())
|
||||
|
||||
|
||||
get_file_path = common_utils.ChunkedPath("users/avatars", preserve_file_name=False)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class User(AbstractUser):
|
||||
|
||||
|
@ -88,6 +95,19 @@ class User(AbstractUser):
|
|||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
avatar = VersatileImageField(
|
||||
upload_to=get_file_path,
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=150,
|
||||
validators=[
|
||||
common_validators.ImageDimensionsValidator(min_width=50, min_height=50),
|
||||
common_validators.FileValidator(
|
||||
allowed_extensions=["png", "jpg", "jpeg", "gif"],
|
||||
max_size=1024 * 1024 * 2,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
|
|
@ -3,6 +3,8 @@ from rest_auth.serializers import PasswordResetSerializer as PRS
|
|||
from rest_auth.registration.serializers import RegisterSerializer as RS
|
||||
from rest_framework import serializers
|
||||
|
||||
from versatileimagefield.serializers import VersatileImageFieldSerializer
|
||||
|
||||
from funkwhale_api.activity import serializers as activity_serializers
|
||||
|
||||
from . import models
|
||||
|
@ -49,15 +51,29 @@ class UserBasicSerializer(serializers.ModelSerializer):
|
|||
fields = ["id", "username", "name", "date_joined"]
|
||||
|
||||
|
||||
avatar_field = VersatileImageFieldSerializer(
|
||||
allow_null=True,
|
||||
sizes=[
|
||||
("original", "url"),
|
||||
("square_crop", "crop__400x400"),
|
||||
("medium_square_crop", "crop__200x200"),
|
||||
("small_square_crop", "crop__50x50"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class UserWriteSerializer(serializers.ModelSerializer):
|
||||
avatar = avatar_field
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["name", "privacy_level"]
|
||||
fields = ["name", "privacy_level", "avatar"]
|
||||
|
||||
|
||||
class UserReadSerializer(serializers.ModelSerializer):
|
||||
|
||||
permissions = serializers.SerializerMethodField()
|
||||
avatar = avatar_field
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
|
@ -71,6 +87,7 @@ class UserReadSerializer(serializers.ModelSerializer):
|
|||
"permissions",
|
||||
"date_joined",
|
||||
"privacy_level",
|
||||
"avatar",
|
||||
]
|
||||
|
||||
def get_permissions(self, o):
|
||||
|
|
|
@ -37,7 +37,7 @@ oauth2client<4
|
|||
google-api-python-client>=1.6,<1.7
|
||||
arrow>=0.12,<0.13
|
||||
persisting-theory>=0.2,<0.3
|
||||
django-versatileimagefield>=1.8,<1.9
|
||||
django-versatileimagefield>=1.9,<1.10
|
||||
django-filter>=1.1,<1.2
|
||||
django-rest-auth>=0.9,<0.10
|
||||
beautifulsoup4>=4.6,<4.7
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import datetime
|
||||
import io
|
||||
import PIL
|
||||
import random
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
|
@ -258,3 +261,14 @@ def now(mocker):
|
|||
now = timezone.now()
|
||||
mocker.patch("django.utils.timezone.now", return_value=now)
|
||||
return now
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def avatar():
|
||||
i = PIL.Image.new("RGBA", (400, 400), random.choice(["red", "blue", "yellow"]))
|
||||
f = io.BytesIO()
|
||||
i.save(f, "png")
|
||||
f.name = "avatar.png"
|
||||
f.seek(0)
|
||||
yield f
|
||||
f.close()
|
||||
|
|
|
@ -235,3 +235,17 @@ def test_user_cannot_patch_another_user(method, logged_in_api_client, factories)
|
|||
response = handler(url, payload)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_user_can_patch_their_own_avatar(logged_in_api_client, avatar):
|
||||
user = logged_in_api_client.user
|
||||
url = reverse("api:v1:users:users-detail", kwargs={"username": user.username})
|
||||
content = avatar.read()
|
||||
avatar.seek(0)
|
||||
payload = {"avatar": avatar}
|
||||
response = logged_in_api_client.patch(url, payload)
|
||||
|
||||
assert response.status_code == 200
|
||||
user.refresh_from_db()
|
||||
|
||||
assert user.avatar.read() == content
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Users can now upload an avatar in their settings page (#257)
|
|
@ -39,6 +39,7 @@
|
|||
<translate :translate-params="{username: $store.state.auth.username}">
|
||||
Logged in as %{ username }
|
||||
</translate>
|
||||
<img class="ui avatar right floated circular mini image" v-if="$store.state.auth.profile.avatar.square_crop" :src="$store.getters['instance/absoluteUrl']($store.state.auth.profile.avatar.square_crop)" />
|
||||
</router-link>
|
||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i><translate>Logout</translate></router-link>
|
||||
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i><translate>Login</translate></router-link>
|
||||
|
@ -432,4 +433,8 @@ $sidebar-color: #3d3e3f;
|
|||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
top: -0.5em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
<div v-if="isLoading" class="ui vertical segment">
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
|
||||
</div>
|
||||
<template v-if="$store.state.auth.profile">
|
||||
<template v-if="profile">
|
||||
<div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']">
|
||||
<h2 class="ui center aligned icon header">
|
||||
<i class="circular inverted user green icon"></i>
|
||||
<i v-if="!profile.avatar.square_crop" class="circular inverted user green icon"></i>
|
||||
<img class="ui big circular image" v-else :src="$store.getters['instance/absoluteUrl'](profile.avatar.square_crop)" />
|
||||
<div class="content">
|
||||
{{ $store.state.auth.profile.username }}
|
||||
{{ profile.username }}
|
||||
<div class="sub header" v-translate="{date: signupDate}">Registered since %{ date }</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="ui basic green label">
|
||||
<translate>This is you!</translate>
|
||||
</div>
|
||||
<div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
|
||||
<div v-if="profile.is_staff" class="ui yellow label">
|
||||
<i class="star icon"></i>
|
||||
<translate>Staff member</translate>
|
||||
</div>
|
||||
|
@ -30,15 +31,20 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
const dateFormat = require('dateformat')
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
props: ['username'],
|
||||
created () {
|
||||
this.$store.dispatch('auth/fetchProfile')
|
||||
},
|
||||
computed: {
|
||||
|
||||
...mapState({
|
||||
profile: state => state.auth.profile
|
||||
}),
|
||||
labels () {
|
||||
let msg = this.$gettext('%{ username }\'s profile')
|
||||
let usernameProfile = this.$gettextInterpolate(msg, {username: this.username})
|
||||
|
@ -47,11 +53,11 @@ export default {
|
|||
}
|
||||
},
|
||||
signupDate () {
|
||||
let d = new Date(this.$store.state.auth.profile.date_joined)
|
||||
let d = new Date(this.profile.date_joined)
|
||||
return dateFormat(d, 'longDate')
|
||||
},
|
||||
isLoading () {
|
||||
return !this.$store.state.auth.profile
|
||||
return !this.profile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,4 +65,7 @@ export default {
|
|||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
.ui.header > img.image {
|
||||
width: 8em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,6 +30,39 @@
|
|||
</form>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui small text container">
|
||||
<h2 class="ui header">
|
||||
<translate>Avatar</translate>
|
||||
</h2>
|
||||
<div class="ui form">
|
||||
<div v-if="avatarErrors.length > 0" class="ui negative message">
|
||||
<div class="header"><translate>We cannot save your avatar</translate></div>
|
||||
<ul class="list">
|
||||
<li v-for="error in avatarErrors">{{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ui stackable grid">
|
||||
<div class="ui ten wide column">
|
||||
<h3 class="ui header"><translate>Upload a new avatar</translate></h3>
|
||||
<p><translate>PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px.</translate></p>
|
||||
<input class="ui input" ref="avatar" type="file" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<button @click="submitAvatar" :class="['ui', {'loading': isLoadingAvatar}, 'button']">
|
||||
<translate>Update avatar</translate>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui six wide column">
|
||||
<h3 class="ui header"><translate>Current avatar</translate></h3>
|
||||
<img class="ui circular image" v-if="currentAvatar && currentAvatar.square_crop" :src="$store.getters['instance/absoluteUrl'](currentAvatar.medium_square_crop)" />
|
||||
<div class="ui hidden divider"></div>
|
||||
<button @click="removeAvatar" v-if="currentAvatar && currentAvatar.square_crop" :class="['ui', {'loading': isLoadingAvatar}, ,'yellow', 'button']">
|
||||
<translate>Remove avatar</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui small text container">
|
||||
<h2 class="ui header">
|
||||
<translate>Change my password</translate>
|
||||
|
@ -97,8 +130,12 @@ export default {
|
|||
// properties that will be used in it
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
currentAvatar: this.$store.state.auth.profile.avatar,
|
||||
passwordError: '',
|
||||
isLoading: false,
|
||||
isLoadingAvatar: false,
|
||||
avatarErrors: [],
|
||||
avatar: null,
|
||||
settings: {
|
||||
success: false,
|
||||
errors: [],
|
||||
|
@ -147,6 +184,46 @@ export default {
|
|||
self.settings.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
submitAvatar () {
|
||||
this.isLoadingAvatar = true
|
||||
this.avatarErrors = []
|
||||
let self = this
|
||||
this.avatar = this.$refs.avatar.files[0]
|
||||
let formData = new FormData()
|
||||
formData.append('avatar', this.avatar)
|
||||
axios.patch(
|
||||
`users/users/${this.$store.state.auth.username}/`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
).then(response => {
|
||||
this.isLoadingAvatar = false
|
||||
self.currentAvatar = response.data.avatar
|
||||
self.$store.commit('auth/avatar', self.currentAvatar)
|
||||
}, error => {
|
||||
self.isLoadingAvatar = false
|
||||
self.avatarErrors = error.backendErrors
|
||||
})
|
||||
},
|
||||
removeAvatar () {
|
||||
this.isLoadingAvatar = true
|
||||
let self = this
|
||||
this.avatar = null
|
||||
axios.patch(
|
||||
`users/users/${this.$store.state.auth.username}/`,
|
||||
{avatar: null}
|
||||
).then(response => {
|
||||
this.isLoadingAvatar = false
|
||||
self.currentAvatar = {}
|
||||
self.$store.commit('auth/avatar', self.currentAvatar)
|
||||
}, error => {
|
||||
self.isLoadingAvatar = false
|
||||
self.avatarErrors = error.backendErrors
|
||||
})
|
||||
},
|
||||
submitPassword () {
|
||||
var self = this
|
||||
self.isLoading = true
|
||||
|
|
|
@ -47,6 +47,11 @@ export default {
|
|||
username: (state, value) => {
|
||||
state.username = value
|
||||
},
|
||||
avatar: (state, value) => {
|
||||
if (state.profile) {
|
||||
state.profile.avatar = value
|
||||
}
|
||||
},
|
||||
token: (state, value) => {
|
||||
state.token = value
|
||||
if (value) {
|
||||
|
|
Ładowanie…
Reference in New Issue