Skip to content

Commit

Permalink
[IMPROVE] Add permission to change other user profile avatar (#13884)
Browse files Browse the repository at this point in the history
* Fixes #12312

* Add privilage for admin to update as much profile images as they want

* Added API fix too

* Check for permissions

* Check permission in rest endpoint

* Handle pass own id to avatar set methods

* Code improvements
  • Loading branch information
knrt10 authored and rodrigok committed Apr 6, 2019
1 parent 8121c08 commit 05ee4dc
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 23 deletions.
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,9 +5,11 @@ 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 { settings } from '../../../settings';
import toastr from 'toastr';
import { callbacks } from '../../../callbacks';
import s from 'underscore.string';

Template.userEdit.helpers({
Expand All @@ -19,10 +21,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 @@ -42,6 +57,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 @@ -99,7 +162,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 @@ -163,7 +228,6 @@ Template.userEdit.onCreated(function() {
return;
}
const userData = this.getUserData();

if (this.user != null) {
for (const key in userData) {
if (key) {
Expand All @@ -177,6 +241,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

0 comments on commit 05ee4dc

Please sign in to comment.