Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 960893d

Browse files
ChristophWursthamza221
andcommittedMay 10, 2023
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>
1 parent f63c2db commit 960893d

18 files changed

+267
-18
lines changed
 

‎apps/dav/lib/CardDAV/Converter.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@
3131
use OCP\Accounts\IAccountManager;
3232
use OCP\IImage;
3333
use OCP\IUser;
34+
use OCP\IUserManager;
3435
use Sabre\VObject\Component\VCard;
3536
use Sabre\VObject\Property\Text;
3637

3738
class Converter {
3839
/** @var IAccountManager */
3940
private $accountManager;
41+
private IUserManager $userManager;
4042

41-
public function __construct(IAccountManager $accountManager) {
43+
public function __construct(IAccountManager $accountManager,
44+
IUserManager $userManager) {
4245
$this->accountManager = $accountManager;
46+
$this->userManager = $userManager;
4347
}
4448

4549
public function createCardFromUser(IUser $user): ?VCard {
@@ -102,6 +106,20 @@ public function createCardFromUser(IUser $user): ?VCard {
102106
}
103107
}
104108

109+
// Local properties
110+
$managers = $user->getManagerUids();
111+
// X-MANAGERSNAME only allows a single value, so we take the first manager
112+
if (isset($managers[0])) {
113+
$displayName = $this->userManager->getDisplayName($managers[0]);
114+
// Only set the manager if a user object is found
115+
if ($displayName !== null) {
116+
$vCard->add(new Text($vCard, 'X-MANAGERSNAME', $displayName, [
117+
'uid' => $managers[0],
118+
'X-NC-SCOPE' => IAccountManager::SCOPE_LOCAL,
119+
]));
120+
}
121+
}
122+
105123
if ($publish && !empty($cloudId)) {
106124
$vCard->add(new Text($vCard, 'CLOUD', $cloudId));
107125
$vCard->validate();

‎apps/dav/tests/unit/CardDAV/ConverterTest.php

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<?php
2+
3+
declare(strict_types=1);
4+
25
/**
36
* @copyright Copyright (c) 2016, ownCloud, Inc.
47
*
@@ -33,18 +36,22 @@
3336
use OCP\Accounts\IAccountProperty;
3437
use OCP\IImage;
3538
use OCP\IUser;
39+
use OCP\IUserManager;
3640
use PHPUnit\Framework\MockObject\MockObject;
3741
use Test\TestCase;
3842

3943
class ConverterTest extends TestCase {
4044

4145
/** @var IAccountManager|\PHPUnit\Framework\MockObject\MockObject */
4246
private $accountManager;
47+
/** @var IUserManager|(IUserManager&MockObject)|MockObject */
48+
private IUserManager|MockObject $userManager;
4349

4450
protected function setUp(): void {
4551
parent::setUp();
4652

4753
$this->accountManager = $this->createMock(IAccountManager::class);
54+
$this->userManager = $this->createMock(IUserManager::class);
4855
}
4956

5057
/**
@@ -96,7 +103,7 @@ public function testCreation($expectedVCard, $displayName = null, $eMailAddress
96103
$user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
97104
$accountManager = $this->getAccountManager($user);
98105

99-
$converter = new Converter($accountManager);
106+
$converter = new Converter($accountManager, $this->userManager);
100107
$vCard = $converter->createCardFromUser($user);
101108
if ($expectedVCard !== null) {
102109
$this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
@@ -107,6 +114,29 @@ public function testCreation($expectedVCard, $displayName = null, $eMailAddress
107114
}
108115
}
109116

117+
public function testManagerProp(): void {
118+
$user = $this->getUserMock("user", "user@domain.tld", "user@cloud.domain.tld");
119+
$user->method('getManagerUids')
120+
->willReturn(['mgr']);
121+
$this->userManager->expects(self::once())
122+
->method('getDisplayName')
123+
->with('mgr')
124+
->willReturn('Manager');
125+
$accountManager = $this->getAccountManager($user);
126+
127+
$converter = new Converter($accountManager, $this->userManager);
128+
$vCard = $converter->createCardFromUser($user);
129+
130+
$this->compareData(
131+
[
132+
'cloud' => 'user@cloud.domain.tld',
133+
'email' => 'user@domain.tld',
134+
'x-managersname' => 'Manager',
135+
],
136+
$vCard->jsonSerialize()
137+
);
138+
}
139+
110140
protected function compareData($expected, $data) {
111141
foreach ($expected as $key => $value) {
112142
$found = false;
@@ -182,7 +212,7 @@ public function providesNewUsers() {
182212
* @param $fullName
183213
*/
184214
public function testNameSplitter($expected, $fullName): void {
185-
$converter = new Converter($this->accountManager);
215+
$converter = new Converter($this->accountManager, $this->userManager);
186216
$r = $converter->splitFullName($fullName);
187217
$r = implode(';', $r);
188218
$this->assertEquals($expected, $r);

‎apps/provisioning_api/lib/Controller/AUserData.php

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ abstract class AUserData extends OCSController {
6060
public const USER_FIELD_LOCALE = 'locale';
6161
public const USER_FIELD_PASSWORD = 'password';
6262
public const USER_FIELD_QUOTA = 'quota';
63+
public const USER_FIELD_MANAGER = 'manager';
6364
public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email';
6465

6566
/** @var IUserManager */
@@ -151,6 +152,8 @@ protected function getUserData(string $userId, bool $includeScopes = false): arr
151152
$data['backend'] = $targetUserObject->getBackendClassName();
152153
$data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID());
153154
$data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject->getUID());
155+
$managerUids = $targetUserObject->getManagerUids();
156+
$data[self::USER_FIELD_MANAGER] = empty($managerUids) ? '' : $managerUids[0];
154157

155158
try {
156159
if ($includeScopes) {

‎apps/provisioning_api/lib/Controller/UsersController.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,8 @@ public function addUser(
338338
array $groups = [],
339339
array $subadmin = [],
340340
string $quota = '',
341-
string $language = ''
341+
string $language = '',
342+
?string $manager = null,
342343
): DataResponse {
343344
$user = $this->userSession->getUser();
344345
$isAdmin = $this->groupManager->isAdmin($user->getUID());
@@ -447,6 +448,15 @@ public function addUser(
447448
$this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
448449
}
449450

451+
/**
452+
* null -> nothing sent
453+
* '' -> unset manager
454+
* else -> set manager
455+
*/
456+
if ($manager !== null) {
457+
$this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
458+
}
459+
450460
// Send new user mail only if a mail is set
451461
if ($email !== '') {
452462
$newUser->setEMailAddress($email);
@@ -800,9 +810,11 @@ public function editUser(string $userId, string $key, string $value): DataRespon
800810

801811
$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
802812

803-
// If admin they can edit their own quota
813+
// If admin they can edit their own quota and manager
804814
if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
805815
$permittedFields[] = self::USER_FIELD_QUOTA;
816+
$permittedFields[] = self::USER_FIELD_MANAGER;
817+
806818
}
807819
} else {
808820
// Check if admin / subadmin
@@ -836,6 +848,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
836848
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
837849
$permittedFields[] = self::USER_FIELD_QUOTA;
838850
$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
851+
$permittedFields[] = self::USER_FIELD_MANAGER;
839852
} else {
840853
// No rights
841854
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
@@ -885,6 +898,9 @@ public function editUser(string $userId, string $key, string $value): DataRespon
885898
}
886899
$targetUser->setQuota($quota);
887900
break;
901+
case self::USER_FIELD_MANAGER:
902+
$targetUser->setManagerUids([$value]);
903+
break;
888904
case self::USER_FIELD_PASSWORD:
889905
try {
890906
if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {

‎apps/settings/css/settings.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/settings/css/settings.css.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/settings/css/settings.scss

+3
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
13461346
minmax($grid-col-min-width, 1fr) // email
13471347
minmax(1.5*$grid-col-min-width, 1fr) // groups
13481348
minmax(1.5*$grid-col-min-width, 1fr) // group admins
1349+
minmax($grid-col-min-width, 1fr) // quota
1350+
minmax(1.5*$grid-col-min-width, 1fr) // manager
13491351
repeat(auto-fit, minmax($grid-col-min-width, 1fr));
13501352
border-bottom: var(--color-border) 1px solid;
13511353

@@ -1390,6 +1392,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
13901392
}
13911393
}
13921394

1395+
.managers
13931396
.groups,
13941397
.subadmins,
13951398
.quota {

‎apps/settings/src/components/UserList.vue

+33
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@
142142
<div v-if="showConfig.showStoragePath" class="storageLocation" />
143143
<div v-if="showConfig.showUserBackend" class="userBackend" />
144144
<div v-if="showConfig.showLastLogin" class="lastLogin" />
145+
<div :class="{'icon-loading-small': loading.manager}" class="modal__item managers">
146+
<NcMultiselect ref="manager"
147+
v-model="newUser.manager"
148+
:close-on-select="true"
149+
:user-select="true"
150+
:options="possibleManagers"
151+
:placeholder="t('settings', 'Select user manager')"
152+
class="multiselect-vue"
153+
@search-change="searchUserManager"
154+
label="displayname"
155+
track-by="id">
156+
<span slot="noResult">{{ t('settings', 'No results') }}</span>
157+
</NcMultiselect>
158+
</div>
145159
<div class="user-actions">
146160
<NcButton id="newsubmit"
147161
type="primary"
@@ -201,6 +215,9 @@
201215
class="headerLastLogin lastLogin">
202216
{{ t('settings', 'Last login') }}
203217
</div>
218+
<div id="headerManager" class="manager">
219+
{{ t('settings', 'Manager') }}
220+
</div>
204221

205222
<div class="userActions" />
206223
</div>
@@ -215,6 +232,7 @@
215232
:show-config="showConfig"
216233
:sub-admins-groups="subAdminsGroups"
217234
:user="user"
235+
:users="users"
218236
:is-dark-theme="isDarkTheme" />
219237
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
220238
<div slot="spinner">
@@ -257,6 +275,7 @@ const newUser = {
257275
password: '',
258276
mailAddress: '',
259277
groups: [],
278+
manager: '',
260279
subAdminsGroups: [],
261280
quota: defaultQuota,
262281
language: {
@@ -301,6 +320,7 @@ export default {
301320
groups: false,
302321
},
303322
scrolled: false,
323+
possibleManagers: [],
304324
searchQuery: '',
305325
newUser: Object.assign({}, newUser),
306326
}
@@ -411,6 +431,10 @@ export default {
411431
},
412432
},
413433

434+
async beforeMount() {
435+
await this.searchUserManager()
436+
},
437+
414438
mounted() {
415439
if (!this.settings.canChangePassword) {
416440
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
@@ -438,6 +462,14 @@ export default {
438462
},
439463

440464
methods: {
465+
async searchUserManager(query) {
466+
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
467+
const users = response?.data ? Object.values(response?.data.ocs.data.users) : []
468+
if (users.length > 0) {
469+
this.possibleManagers = users
470+
}
471+
})
472+
},
441473
onScroll(event) {
442474
this.scrolled = event.target.scrollTo > 0
443475
},
@@ -521,6 +553,7 @@ export default {
521553
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
522554
quota: this.newUser.quota.id,
523555
language: this.newUser.language.code,
556+
manager: this.newUser.manager.id,
524557
})
525558
.then(() => {
526559
this.resetForm()

‎apps/settings/src/components/UserList/UserRow.vue

+58
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,22 @@
214214
track-by="code"
215215
@input="setUserLanguage" />
216216
</div>
217+
<div :class="{'icon-loading-small': loading.manager}" class="managers">
218+
<NcMultiselect ref="manager"
219+
v-model="currentManager"
220+
:close-on-select="true"
221+
:user-select="true"
222+
:options="possibleManagers"
223+
:placeholder="t('settings', 'Select manager')"
224+
class="multiselect-vue"
225+
label="displayname"
226+
track-by="id"
227+
@search-change="searchUserManager"
228+
@remove="updateUserManager"
229+
@select="updateUserManager">
230+
<span slot="noResult">{{ t('settings', 'No results') }}</span>
231+
</NcMultiselect>
232+
</div>
217233

218234
<!-- don't show this on edit mode -->
219235
<div v-if="showConfig.showStoragePath || showConfig.showUserBackend"
@@ -272,6 +288,10 @@ export default {
272288
},
273289
mixins: [UserRowMixin],
274290
props: {
291+
users: {
292+
type: Array,
293+
required: true,
294+
},
275295
user: {
276296
type: Object,
277297
required: true,
@@ -314,6 +334,8 @@ export default {
314334
rand: parseInt(Math.random() * 1000),
315335
openedMenu: false,
316336
feedbackMessage: '',
337+
possibleManagers: [],
338+
currentManager: '',
317339
editing: false,
318340
loading: {
319341
all: false,
@@ -327,10 +349,12 @@ export default {
327349
disable: false,
328350
languages: false,
329351
wipe: false,
352+
manager: false,
330353
},
331354
}
332355
},
333356
computed: {
357+
334358
/* USER POPOVERMENU ACTIONS */
335359
userActions() {
336360
const actions = [
@@ -360,6 +384,12 @@ export default {
360384
return actions.concat(this.externalActions)
361385
},
362386
},
387+
async beforeMount() {
388+
await this.searchUserManager()
389+
if (this.user.manager) {
390+
await this.initManager(this.user.manager)
391+
}
392+
},
363393

364394
methods: {
365395
/* MENU HANDLING */
@@ -396,6 +426,34 @@ export default {
396426
)
397427
},
398428

429+
filterManagers(managers) {
430+
return managers.filter((manager) => manager.id !== this.user.id)
431+
},
432+
async initManager(userId) {
433+
await this.$store.dispatch('getUser', userId).then(response => {
434+
this.currentManager = response?.data.ocs.data
435+
})
436+
},
437+
async searchUserManager(query) {
438+
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
439+
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
440+
if (users.length > 0) {
441+
this.possibleManagers = users
442+
}
443+
})
444+
},
445+
446+
updateUserManager(manager) {
447+
this.loading.manager = true
448+
this.$store.dispatch('setUserData', {
449+
userid: this.user.id,
450+
key: 'manager',
451+
value: this.currentManager ? this.currentManager.id : '',
452+
}).then(() => {
453+
this.loading.manager = false
454+
})
455+
},
456+
399457
deleteUser() {
400458
const userid = this.user.id
401459
OC.dialogs.confirmDestructive(

‎apps/settings/src/components/UserList/UserRowSimple.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
<div v-if="showConfig.showLastLogin" :title="userLastLoginTooltip" class="lastLogin">
5656
{{ userLastLogin }}
5757
</div>
58-
58+
<div class="managers">
59+
{{ user.manager }}
60+
</div>
5961
<div class="userActions">
6062
<div v-if="canEdit && !loading.all" class="toggleUserActions">
6163
<NcActions>

‎apps/settings/src/store/users.js

+40-4
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,41 @@ let searchRequestCancelSource = null
253253

254254
const actions = {
255255

256+
/**
257+
* search users
258+
*
259+
* @param {object} context store context
260+
* @param {object} options destructuring object
261+
* @param {number} options.offset List offset to request
262+
* @param {number} options.limit List number to return from offset
263+
* @param {string} options.search Search amongst users
264+
* @return {Promise}
265+
*/
266+
searchUsers(context, { offset, limit, search }) {
267+
search = typeof search === 'string' ? search : ''
268+
269+
return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search })).catch((error) => {
270+
if (!axios.isCancel(error)) {
271+
context.commit('API_FAILURE', error)
272+
}
273+
})
274+
},
275+
276+
/**
277+
* Get user details
278+
*
279+
* @param {object} context store context
280+
* @param {string} userId user id
281+
* @return {Promise}
282+
*/
283+
getUser(context, userId) {
284+
return api.get(generateOcsUrl(`cloud/users/${userId}`)).catch((error) => {
285+
if (!axios.isCancel(error)) {
286+
context.commit('API_FAILURE', error)
287+
}
288+
})
289+
},
290+
256291
/**
257292
* Get all users with full details
258293
*
@@ -548,11 +583,12 @@ const actions = {
548583
* @param {string} options.subadmin User subadmin groups
549584
* @param {string} options.quota User email
550585
* @param {string} options.language User language
586+
* @param {string} options.manager User manager
551587
* @return {Promise}
552588
*/
553-
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language }) {
589+
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language, manager }) {
554590
return api.requireAdmin().then((response) => {
555-
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language })
591+
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language, manager })
556592
.then((response) => dispatch('addUserData', userid || response.data.ocs.data.id))
557593
.catch((error) => { throw error })
558594
}).catch((error) => {
@@ -605,8 +641,8 @@ const actions = {
605641
* @return {Promise}
606642
*/
607643
setUserData(context, { userid, key, value }) {
608-
const allowedEmpty = ['email', 'displayname']
609-
if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
644+
const allowedEmpty = ['email', 'displayname', 'manager']
645+
if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) {
610646
// We allow empty email or displayname
611647
if (typeof value === 'string'
612648
&& (

‎dist/settings-users-8351.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/settings-users-8351.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/settings-vue-settings-apps-users-management.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/settings-vue-settings-apps-users-management.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/private/User/LazyUser.php

+8
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,12 @@ public function getQuota() {
159159
public function setQuota($quota) {
160160
$this->getUser()->setQuota($quota);
161161
}
162+
163+
public function getManagerUids(): array {
164+
return $this->getUser()->getManagerUids();
165+
}
166+
167+
public function setManagerUids(array $uids): void {
168+
$this->getUser()->setManagerUids($uids);
169+
}
162170
}

‎lib/private/User/User.php

+25
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@
5959
use OCP\UserInterface;
6060
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
6161
use Symfony\Component\EventDispatcher\GenericEvent;
62+
use function json_decode;
63+
use function json_encode;
6264

6365
class User implements IUser {
66+
private const CONFIG_KEY_MANAGERS = 'manager';
67+
6468
/** @var IAccountManager */
6569
protected $accountManager;
6670
/** @var string */
@@ -532,6 +536,27 @@ public function setQuota($quota) {
532536
\OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
533537
}
534538

539+
public function getManagerUids(): array {
540+
$encodedUids = $this->config->getUserValue(
541+
$this->uid,
542+
'settings',
543+
self::CONFIG_KEY_MANAGERS,
544+
'[]'
545+
);
546+
return json_decode($encodedUids, false, 512, JSON_THROW_ON_ERROR);
547+
}
548+
549+
public function setManagerUids(array $uids): void {
550+
$oldUids = $this->getManagerUids();
551+
$this->config->setUserValue(
552+
$this->uid,
553+
'settings',
554+
self::CONFIG_KEY_MANAGERS,
555+
json_encode($uids, JSON_THROW_ON_ERROR)
556+
);
557+
$this->triggerChange('managers', $uids, $oldUids);
558+
}
559+
535560
/**
536561
* get the avatar image if it exists
537562
*

‎lib/public/IUser.php

+17
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,21 @@ public function getQuota();
270270
* @since 9.0.0
271271
*/
272272
public function setQuota($quota);
273+
274+
/**
275+
* Get the user's manager UIDs
276+
*
277+
* @since 27.0.0
278+
* @return string[]
279+
*/
280+
public function getManagerUids(): array;
281+
282+
/**
283+
* Set the user's manager UIDs
284+
*
285+
* @param string[] $uids UIDs of all managers
286+
* @return void
287+
* @since 27.0.0
288+
*/
289+
public function setManagerUids(array $uids): void;
273290
}

0 commit comments

Comments
 (0)
Please sign in to comment.