Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPROVE] Add permission to change other user profile avatar #13884

Merged
merged 8 commits into from
Apr 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/server/v1/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, {
let user;
if (this.isUserFromParams()) {
user = Meteor.users.findOne(this.userId);
} else if (hasPermission(this.userId, 'edit-other-user-info')) {
} else if (hasPermission(this.userId, 'edit-other-user-avatar')) {
user = this.getUserFromParams();
} else {
return API.v1.unauthorized();
Expand Down
1 change: 1 addition & 0 deletions app/authorization/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Meteor.startup(function() {
{ _id: 'edit-other-user-active-status', roles : ['admin'] },
{ _id: 'edit-other-user-info', roles : ['admin'] },
{ _id: 'edit-other-user-password', roles : ['admin'] },
{ _id: 'edit-other-user-avatar', roles : ['admin'] },
{ _id: 'edit-privileged-setting', roles : ['admin'] },
{ _id: 'edit-room', roles : ['admin', 'owner', 'moderator'] },
{ _id: 'edit-room-retention-policy', roles : ['admin'] },
Expand Down
80 changes: 65 additions & 15 deletions app/ui-flextab/client/tabs/userEdit.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,74 @@
<p class="secondary-font-color">{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<form class="" action="index.html" method="post" autocomplete="off">
<div class="rc-input rc-form-group rc-form-group--small">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Name"}}</div>
<div class="rc-input__wrapper">
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="name" autocomplete="off" value="{{user.name}}"/>
</div>
<div class="rc-form-group rc-form-group--small">
<label class="rc-form-label">
{{_ "Profile_picture"}}
</label>
</div>
<div class="rc-input rc-form-group rc-form-group--small">
<label class="rc-input__label">
<div class="rc-input__title">{{_ "Username"}}</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon icon='at' }}
<div class="rc-select-avatar">
<div class="rc-select-avatar__preview">
{{#if avatarPreview}}
{{#if $eq avatarPreview.service 'initials'}}
{{> avatar username=avatarPreview.blob}}
{{else}}
{{> avatar url=avatarPreview.blob}}
{{/if}}
{{else}}
{{> avatar username=user.username}}
{{/if}}
</div>

<div class="rc-select-avatar__list">
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-initials" aria-label="{{_ "initials_avatar" }}">
{{> avatar username=initialsUsername }}
</div>
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-upload" aria-label="{{_ "Upload_user_avatar" }}">
<label class="rc-select-avatar__upload-label avatar" for="upload-avatar">
{{> icon block="rc-select-avatar__upload-icon" icon="upload"}}
</label>
<input type="file" name="" value="" id="upload-avatar" style="display:none;" accept="image/x-png,image/gif,image/jpeg">
</div>
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-url {{selectUrl}}" aria-label="{{_ "Use_url_for_avatar" }}">
<label class="rc-select-avatar__upload-label avatar">
{{> icon block="rc-select-avatar__upload-icon" icon="permalink"}}
</label>
</div>

<div class="rc-input">
<label class="rc-input__label">
<div class="rc-input__title">
{{_ "Use_url_for_avatar"}}
</div>
<div class="rc-input__wrapper">
<input name="avatar_url" class="rc-input__element js-avatar-url-input" placeholder="{{_ "Use_url_for_avatar"}}">
</div>
</label>
</div>
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="username" autocomplete="off" value="{{user.username}}"/>
</div>
</label>
</div>
</div>
<div class="rc-input rc-form-group rc-form-group--small">
<label class="rc-input__label">
<div class="rc-input__title">
{{_ "Name"}}
</div>
<div class="rc-input__wrapper">
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="name" autocomplete="off" value="{{user.name}}"/>
</div>
</label>
</div>
<div class="rc-input rc-form-group rc-form-group--small">
<label class="rc-input__label">
<div class="rc-input__title">
{{_ "Username"}}
</div>
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon icon='at' }}
</div>
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="username" autocomplete="off" value="{{user.username}}"/>
</div>
</label>
</div>
<div class="rc-form-group rc-form-group--small">
<div class="rc-input">
Expand Down
90 changes: 88 additions & 2 deletions app/ui-flextab/client/tabs/userEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import { t, handleError } from '../../../utils';
import { Roles } from '../../../models';
import { Notifications } from '../../../notifications';
import { hasAtLeastOnePermission } from '../../../authorization';
import toastr from 'toastr';
import { callbacks } from '../../../callbacks';
import s from 'underscore.string';

Template.userEdit.helpers({
Expand All @@ -18,10 +20,23 @@ Template.userEdit.helpers({
return (Template.instance().user && hasAtLeastOnePermission('edit-other-user-info')) || (!Template.instance().user && hasAtLeastOnePermission('create-user'));
},

selectUrl() {
return Template.instance().url.get().trim() ? '' : 'disabled';
},

user() {
return Template.instance().user;
},

initialsUsername() {
const { user } = Template.instance();
return `@${ user && user.username }`;
},

avatarPreview() {
return Template.instance().avatar.get();
},

requirePasswordChange() {
return !Template.instance().user || Template.instance().user.requirePasswordChange;
},
Expand All @@ -41,6 +56,54 @@ Template.userEdit.helpers({
});

Template.userEdit.events({
'click .js-select-avatar-initials'(e, template) {
template.avatar.set({
service: 'initials',
blob: `@${ template.user.username }`,
});
},

'click .js-select-avatar-url'(e, template) {
const url = template.url.get().trim();
if (!url) {
return;
}

template.avatar.set({
service: 'url',
contentType: '',
blob: url,
});
},

'input .js-avatar-url-input'(e, template) {
const text = e.target.value;
template.url.set(text);
},

'change .js-select-avatar-upload [type=file]'(event, template) {
const e = event.originalEvent || event;
let { files } = e.target;
if (!files || files.length === 0) {
files = (e.dataTransfer && e.dataTransfer.files) || [];
}
Object.keys(files).forEach((key) => {
const blob = files[key];
if (!/image\/.+/.test(blob.type)) {
return;
}
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
template.avatar.set({
service: 'upload',
contentType: blob.type,
blob: reader.result,
});
};
});
},

'click .cancel'(e, t) {
e.stopPropagation();
e.preventDefault();
Expand Down Expand Up @@ -98,7 +161,9 @@ Template.userEdit.events({
Template.userEdit.onCreated(function() {
this.user = this.data != null ? this.data.user : undefined;
this.roles = this.user ? new ReactiveVar(this.user.roles) : new ReactiveVar([]);

this.avatar = new ReactiveVar;
this.url = new ReactiveVar('');
Notifications.onLogged('updateAvatar', () => this.avatar.set());

const { tabBar } = Template.currentData();

Expand Down Expand Up @@ -162,7 +227,6 @@ Template.userEdit.onCreated(function() {
return;
}
const userData = this.getUserData();

if (this.user != null) {
for (const key in userData) {
if (key) {
Expand All @@ -176,6 +240,28 @@ Template.userEdit.onCreated(function() {
}
}

const avatar = this.avatar.get();
if (avatar) {
let method;
const params = [];

if (avatar.service === 'initials') {
method = 'resetAvatar';
} else {
method = 'setAvatarFromService';
params.push(avatar.blob, avatar.contentType, avatar.service);
}

Meteor.call(method, ...params, Template.instance().user._id, function(err) {
if (err && err.details) {
toastr.error(t(err.message));
} else {
toastr.success(t('Avatar_changed_successfully'));
callbacks.run('userAvatarSet', avatar.service);
}
});
}

Meteor.call('insertOrUpdateUser', userData, (error) => {
if (error) {
return handleError(error);
Expand Down
2 changes: 2 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,8 @@
"edit-message_description": "Permission to edit a message within a room",
"edit-other-user-active-status": "Edit Other User Active Status",
"edit-other-user-active-status_description": "Permission to enable or disable other accounts",
"edit-other-user-avatar": "Edit Other User Avatar",
"edit-other-user-avatar_description": "Permission to change other user's avatar.",
"edit-other-user-info": "Edit Other User Information",
"edit-other-user-info_description": "Permission to change other user's name, username or email address.",
"edit-other-user-password": "Edit Other User Password",
Expand Down
26 changes: 23 additions & 3 deletions server/methods/resetAvatar.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { FileUpload } from '../../app/file-upload';
import { Users } from '../../app/models';
import { Users } from '../../app/models/server';
import { settings } from '../../app/settings';
import { Notifications } from '../../app/notifications';
import { hasPermission } from '../../app/authorization/server';

Meteor.methods({
resetAvatar() {
resetAvatar(userId) {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'resetAvatar',
Expand All @@ -19,7 +20,26 @@ Meteor.methods({
});
}

const user = Meteor.user();
let user;

if (userId && userId !== Meteor.userId()) {
if (!hasPermission(Meteor.userId(), 'edit-other-user-avatar')) {
throw new Meteor.Error('error-unauthorized', 'Unauthorized', {
method: 'resetAvatar',
});
}

user = Users.findOneById(userId, { fields: { _id: 1, username: 1 } });
} else {
user = Meteor.user();
}

if (user == null) {
throw new Meteor.Error('error-invalid-desired-user', 'Invalid desired user', {
method: 'resetAvatar',
});
}

FileUpload.getStore('Avatars').deleteByName(user.username);
Users.unsetAvatarOrigin(user._id);
Notifications.notifyLogged('updateAvatar', {
Expand Down
25 changes: 23 additions & 2 deletions server/methods/setAvatarFromService.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { Match, check } from 'meteor/check';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { settings } from '../../app/settings';
import { setUserAvatar } from '../../app/lib';
import { Users } from '../../app/models/server';
import { hasPermission } from '../../app/authorization/server';

Meteor.methods({
setAvatarFromService(dataURI, contentType, service) {
setAvatarFromService(dataURI, contentType, service, userId) {
check(dataURI, String);
check(contentType, Match.Optional(String));
check(service, Match.Optional(String));
check(userId, Match.Optional(String));

if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
Expand All @@ -22,7 +25,25 @@ Meteor.methods({
});
}

const user = Meteor.user();
let user;

if (userId && userId !== Meteor.userId()) {
if (!hasPermission(Meteor.userId(), 'edit-other-user-avatar')) {
throw new Meteor.Error('error-unauthorized', 'Unauthorized', {
method: 'setAvatarFromService',
});
}

user = Users.findOneById(userId, { fields: { _id: 1, username: 1 } });
} else {
user = Meteor.user();
}

if (user == null) {
throw new Meteor.Error('error-invalid-desired-user', 'Invalid desired user', {
method: 'setAvatarFromService',
});
}

return setUserAvatar(user, dataURI, contentType, service);
},
Expand Down