feat(users): Store and load a user's manager

Co-Authored-By: hamza221 <hamzamahjoubi221@gmail.com>
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
pull/38013/head
Christoph Wurst 1 year ago
parent 1399c88ee1
commit 1381c4c157
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8

@ -31,15 +31,19 @@ use Exception;
use OCP\Accounts\IAccountManager;
use OCP\IImage;
use OCP\IUser;
use OCP\IUserManager;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
class Converter {
/** @var IAccountManager */
private $accountManager;
private IUserManager $userManager;
public function __construct(IAccountManager $accountManager) {
public function __construct(IAccountManager $accountManager,
IUserManager $userManager) {
$this->accountManager = $accountManager;
$this->userManager = $userManager;
}
public function createCardFromUser(IUser $user): ?VCard {
@ -102,6 +106,20 @@ class Converter {
}
}
// Local properties
$managers = $user->getManagerUids();
// X-MANAGERSNAME only allows a single value, so we take the first manager
if (isset($managers[0])) {
$displayName = $this->userManager->getDisplayName($managers[0]);
// Only set the manager if a user object is found
if ($displayName !== null) {
$vCard->add(new Text($vCard, 'X-MANAGERSNAME', $displayName, [
'uid' => $managers[0],
'X-NC-SCOPE' => IAccountManager::SCOPE_LOCAL,
]));
}
}
if ($publish && !empty($cloudId)) {
$vCard->add(new Text($vCard, 'CLOUD', $cloudId));
$vCard->validate();

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
@ -33,6 +36,7 @@ use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\IImage;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -40,11 +44,14 @@ class ConverterTest extends TestCase {
/** @var IAccountManager|\PHPUnit\Framework\MockObject\MockObject */
private $accountManager;
/** @var IUserManager|(IUserManager&MockObject)|MockObject */
private IUserManager|MockObject $userManager;
protected function setUp(): void {
parent::setUp();
$this->accountManager = $this->createMock(IAccountManager::class);
$this->userManager = $this->createMock(IUserManager::class);
}
/**
@ -96,7 +103,7 @@ class ConverterTest extends TestCase {
$user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
$accountManager = $this->getAccountManager($user);
$converter = new Converter($accountManager);
$converter = new Converter($accountManager, $this->userManager);
$vCard = $converter->createCardFromUser($user);
if ($expectedVCard !== null) {
$this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
@ -107,6 +114,29 @@ class ConverterTest extends TestCase {
}
}
public function testManagerProp(): void {
$user = $this->getUserMock("user", "user@domain.tld", "user@cloud.domain.tld");
$user->method('getManagerUids')
->willReturn(['mgr']);
$this->userManager->expects(self::once())
->method('getDisplayName')
->with('mgr')
->willReturn('Manager');
$accountManager = $this->getAccountManager($user);
$converter = new Converter($accountManager, $this->userManager);
$vCard = $converter->createCardFromUser($user);
$this->compareData(
[
'cloud' => 'user@cloud.domain.tld',
'email' => 'user@domain.tld',
'x-managersname' => 'Manager',
],
$vCard->jsonSerialize()
);
}
protected function compareData($expected, $data) {
foreach ($expected as $key => $value) {
$found = false;
@ -182,7 +212,7 @@ class ConverterTest extends TestCase {
* @param $fullName
*/
public function testNameSplitter($expected, $fullName): void {
$converter = new Converter($this->accountManager);
$converter = new Converter($this->accountManager, $this->userManager);
$r = $converter->splitFullName($fullName);
$r = implode(';', $r);
$this->assertEquals($expected, $r);

@ -60,6 +60,7 @@ abstract class AUserData extends OCSController {
public const USER_FIELD_LOCALE = 'locale';
public const USER_FIELD_PASSWORD = 'password';
public const USER_FIELD_QUOTA = 'quota';
public const USER_FIELD_MANAGER = 'manager';
public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email';
/** @var IUserManager */
@ -151,6 +152,8 @@ abstract class AUserData extends OCSController {
$data['backend'] = $targetUserObject->getBackendClassName();
$data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID());
$data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject->getUID());
$managerUids = $targetUserObject->getManagerUids();
$data[self::USER_FIELD_MANAGER] = empty($managerUids) ? '' : $managerUids[0];
try {
if ($includeScopes) {

@ -338,7 +338,8 @@ class UsersController extends AUserData {
array $groups = [],
array $subadmin = [],
string $quota = '',
string $language = ''
string $language = '',
?string $manager = null,
): DataResponse {
$user = $this->userSession->getUser();
$isAdmin = $this->groupManager->isAdmin($user->getUID());
@ -447,6 +448,15 @@ class UsersController extends AUserData {
$this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
}
/**
* null -> nothing sent
* '' -> unset manager
* else -> set manager
*/
if ($manager !== null) {
$this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
}
// Send new user mail only if a mail is set
if ($email !== '') {
$newUser->setEMailAddress($email);
@ -800,9 +810,11 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
// If admin they can edit their own quota
// If admin they can edit their own quota and manager
if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
$permittedFields[] = self::USER_FIELD_QUOTA;
$permittedFields[] = self::USER_FIELD_MANAGER;
}
} else {
// Check if admin / subadmin
@ -836,6 +848,7 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
$permittedFields[] = self::USER_FIELD_QUOTA;
$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
$permittedFields[] = self::USER_FIELD_MANAGER;
} else {
// No rights
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
@ -885,6 +898,9 @@ class UsersController extends AUserData {
}
$targetUser->setQuota($quota);
break;
case self::USER_FIELD_MANAGER:
$targetUser->setManagerUids([$value]);
break;
case self::USER_FIELD_PASSWORD:
try {
if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {

@ -1093,6 +1093,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@ -1233,6 +1234,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@ -1411,6 +1413,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1350,6 +1350,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
minmax($grid-col-min-width, 1fr) // email
minmax(1.5*$grid-col-min-width, 1fr) // groups
minmax(1.5*$grid-col-min-width, 1fr) // group admins
minmax($grid-col-min-width, 1fr) // quota
minmax(1.5*$grid-col-min-width, 1fr) // manager
repeat(auto-fit, minmax($grid-col-min-width, 1fr));
border-bottom: var(--color-border) 1px solid;
@ -1394,6 +1396,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
}
}
.managers,
.groups,
.subadmins,
.quota {

@ -146,6 +146,20 @@
<div v-if="showConfig.showStoragePath" class="storageLocation" />
<div v-if="showConfig.showUserBackend" class="userBackend" />
<div v-if="showConfig.showLastLogin" class="lastLogin" />
<div :class="{'icon-loading-small': loading.manager}" class="modal__item managers">
<NcMultiselect ref="manager"
v-model="newUser.manager"
:close-on-select="true"
:user-select="true"
:options="possibleManagers"
:placeholder="t('settings', 'Select user manager')"
class="multiselect-vue"
@search-change="searchUserManager"
label="displayname"
track-by="id">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</NcMultiselect>
</div>
<div class="user-actions">
<NcButton id="newsubmit"
type="primary"
@ -208,7 +222,9 @@
class="headerLastLogin lastLogin">
{{ t('settings', 'Last login') }}
</th>
<th id="headerManager" class="manager">
{{ t('settings', 'Manager') }}
</th>
<th class="userActions hidden-visually">
{{ t('settings', 'User actions') }}
</th>
@ -224,6 +240,7 @@
:show-config="showConfig"
:sub-admins-groups="subAdminsGroups"
:user="user"
:users="users"
:is-dark-theme="isDarkTheme" />
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
@ -268,6 +285,7 @@ const newUser = {
password: '',
mailAddress: '',
groups: [],
manager: '',
subAdminsGroups: [],
quota: defaultQuota,
language: {
@ -312,6 +330,7 @@ export default {
groups: false,
},
scrolled: false,
possibleManagers: [],
searchQuery: '',
newUser: Object.assign({}, newUser),
}
@ -422,6 +441,10 @@ export default {
},
},
async beforeMount() {
await this.searchUserManager()
},
mounted() {
if (!this.settings.canChangePassword) {
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
@ -449,6 +472,14 @@ export default {
},
methods: {
async searchUserManager(query) {
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
const users = response?.data ? Object.values(response?.data.ocs.data.users) : []
if (users.length > 0) {
this.possibleManagers = users
}
})
},
onScroll(event) {
this.scrolled = event.target.scrollTo > 0
},
@ -532,6 +563,7 @@ export default {
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
quota: this.newUser.quota.id,
language: this.newUser.language.code,
manager: this.newUser.manager.id,
})
.then(() => {
this.resetForm()

@ -217,6 +217,22 @@
track-by="code"
@input="setUserLanguage" />
</td>
<td :class="{'icon-loading-small': loading.manager}" class="managers">
<NcMultiselect ref="manager"
v-model="currentManager"
:close-on-select="true"
:user-select="true"
:options="possibleManagers"
:placeholder="t('settings', 'Select manager')"
class="multiselect-vue"
label="displayname"
track-by="id"
@search-change="searchUserManager"
@remove="updateUserManager"
@select="updateUserManager">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</NcMultiselect>
</td>
<!-- don't show this on edit mode -->
<td v-if="showConfig.showStoragePath || showConfig.showUserBackend"
@ -275,6 +291,10 @@ export default {
},
mixins: [UserRowMixin],
props: {
users: {
type: Array,
required: true,
},
user: {
type: Object,
required: true,
@ -317,6 +337,8 @@ export default {
rand: parseInt(Math.random() * 1000),
openedMenu: false,
feedbackMessage: '',
possibleManagers: [],
currentManager: '',
editing: false,
loading: {
all: false,
@ -330,10 +352,12 @@ export default {
disable: false,
languages: false,
wipe: false,
manager: false,
},
}
},
computed: {
/* USER POPOVERMENU ACTIONS */
userActions() {
const actions = [
@ -363,6 +387,12 @@ export default {
return actions.concat(this.externalActions)
},
},
async beforeMount() {
await this.searchUserManager()
if (this.user.manager) {
await this.initManager(this.user.manager)
}
},
methods: {
/* MENU HANDLING */
@ -399,6 +429,34 @@ export default {
)
},
filterManagers(managers) {
return managers.filter((manager) => manager.id !== this.user.id)
},
async initManager(userId) {
await this.$store.dispatch('getUser', userId).then(response => {
this.currentManager = response?.data.ocs.data
})
},
async searchUserManager(query) {
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
if (users.length > 0) {
this.possibleManagers = users
}
})
},
updateUserManager(manager) {
this.loading.manager = true
this.$store.dispatch('setUserData', {
userid: this.user.id,
key: 'manager',
value: this.currentManager ? this.currentManager.id : '',
}).then(() => {
this.loading.manager = false
})
},
deleteUser() {
const userid = this.user.id
OC.dialogs.confirmDestructive(

@ -55,7 +55,9 @@
<td v-if="showConfig.showLastLogin" :title="userLastLoginTooltip" class="lastLogin">
{{ userLastLogin }}
</td>
<td class="managers">
{{ user.manager }}
</td>
<td class="userActions">
<div v-if="canEdit && !loading.all" class="toggleUserActions">
<NcActions>

@ -253,6 +253,41 @@ let searchRequestCancelSource = null
const actions = {
/**
* search users
*
* @param {object} context store context
* @param {object} options destructuring object
* @param {number} options.offset List offset to request
* @param {number} options.limit List number to return from offset
* @param {string} options.search Search amongst users
* @return {Promise}
*/
searchUsers(context, { offset, limit, search }) {
search = typeof search === 'string' ? search : ''
return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search })).catch((error) => {
if (!axios.isCancel(error)) {
context.commit('API_FAILURE', error)
}
})
},
/**
* Get user details
*
* @param {object} context store context
* @param {string} userId user id
* @return {Promise}
*/
getUser(context, userId) {
return api.get(generateOcsUrl(`cloud/users/${userId}`)).catch((error) => {
if (!axios.isCancel(error)) {
context.commit('API_FAILURE', error)
}
})
},
/**
* Get all users with full details
*
@ -548,11 +583,12 @@ const actions = {
* @param {string} options.subadmin User subadmin groups
* @param {string} options.quota User email
* @param {string} options.language User language
* @param {string} options.manager User manager
* @return {Promise}
*/
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language }) {
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language, manager }) {
return api.requireAdmin().then((response) => {
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language })
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language, manager })
.then((response) => dispatch('addUserData', userid || response.data.ocs.data.id))
.catch((error) => { throw error })
}).catch((error) => {
@ -605,8 +641,8 @@ const actions = {
* @return {Promise}
*/
setUserData(context, { userid, key, value }) {
const allowedEmpty = ['email', 'displayname']
if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
const allowedEmpty = ['email', 'displayname', 'manager']
if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) {
// We allow empty email or displayname
if (typeof value === 'string'
&& (

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -159,4 +159,12 @@ class LazyUser implements IUser {
public function setQuota($quota) {
$this->getUser()->setQuota($quota);
}
public function getManagerUids(): array {
return $this->getUser()->getManagerUids();
}
public function setManagerUids(array $uids): void {
$this->getUser()->setManagerUids($uids);
}
}

@ -59,8 +59,12 @@ use OCP\User\Backend\IGetHomeBackend;
use OCP\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use function json_decode;
use function json_encode;
class User implements IUser {
private const CONFIG_KEY_MANAGERS = 'manager';
/** @var IAccountManager */
protected $accountManager;
/** @var string */
@ -532,6 +536,27 @@ class User implements IUser {
\OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
}
public function getManagerUids(): array {
$encodedUids = $this->config->getUserValue(
$this->uid,
'settings',
self::CONFIG_KEY_MANAGERS,
'[]'
);
return json_decode($encodedUids, false, 512, JSON_THROW_ON_ERROR);
}
public function setManagerUids(array $uids): void {
$oldUids = $this->getManagerUids();
$this->config->setUserValue(
$this->uid,
'settings',
self::CONFIG_KEY_MANAGERS,
json_encode($uids, JSON_THROW_ON_ERROR)
);
$this->triggerChange('managers', $uids, $oldUids);
}
/**
* get the avatar image if it exists
*

@ -270,4 +270,21 @@ interface IUser {
* @since 9.0.0
*/
public function setQuota($quota);
/**
* Get the user's manager UIDs
*
* @since 27.0.0
* @return string[]
*/
public function getManagerUids(): array;
/**
* Set the user's manager UIDs
*
* @param string[] $uids UIDs of all managers
* @return void
* @since 27.0.0
*/
public function setManagerUids(array $uids): void;
}

Loading…
Cancel
Save