mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 01:30:50 -04:00
feat(provisioning_api): implement editUserMultiField PATCH endpoint
Add atomic multi-field user update with collected validation errors, IAppConfig migration, and corresponding Vuex store action/mutation. Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
parent
01ab485c95
commit
f96730587f
3 changed files with 335 additions and 50 deletions
|
|
@ -36,6 +36,7 @@ use OCP\EventDispatcher\IEventDispatcher;
|
|||
use OCP\Files\IRootFolder;
|
||||
use OCP\Group\ISubAdmin;
|
||||
use OCP\HintException;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
|
|
@ -57,7 +58,8 @@ use Psr\Log\LoggerInterface;
|
|||
* @psalm-import-type Provisioning_APIGroupDetails from ResponseDefinitions
|
||||
* @psalm-import-type Provisioning_APIUserDetails from ResponseDefinitions
|
||||
*/
|
||||
class UsersController extends AUserDataOCSController {
|
||||
class UsersController extends AUserDataOCSController
|
||||
{
|
||||
|
||||
private IL10N $l10n;
|
||||
|
||||
|
|
@ -81,6 +83,7 @@ class UsersController extends AUserDataOCSController {
|
|||
private IEventDispatcher $eventDispatcher,
|
||||
private IPhoneNumberUtil $phoneNumberUtil,
|
||||
private IAppManager $appManager,
|
||||
private IAppConfig $appConfig,
|
||||
) {
|
||||
parent::__construct(
|
||||
$appName,
|
||||
|
|
@ -109,7 +112,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Users returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUsers(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
|
||||
public function getUsers(string $search = '', ?int $limit = null, int $offset = 0): DataResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
$users = [];
|
||||
|
||||
|
|
@ -151,7 +155,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Users details returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
|
||||
public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse
|
||||
{
|
||||
$currentUser = $this->userSession->getUser();
|
||||
$users = [];
|
||||
|
||||
|
|
@ -178,7 +183,7 @@ class UsersController extends AUserDataOCSController {
|
|||
|
||||
$usersDetails = [];
|
||||
foreach ($users as $userId) {
|
||||
$userId = (string)$userId;
|
||||
$userId = (string) $userId;
|
||||
try {
|
||||
$userData = $this->getUserData($userId);
|
||||
} catch (OCSNotFoundException $e) {
|
||||
|
|
@ -213,7 +218,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Disabled users details returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getDisabledUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
|
||||
public function getDisabledUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse
|
||||
{
|
||||
$currentUser = $this->userSession->getUser();
|
||||
if ($currentUser === null) {
|
||||
return new DataResponse(['users' => []]);
|
||||
|
|
@ -234,7 +240,7 @@ class UsersController extends AUserDataOCSController {
|
|||
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
|
||||
if ($isAdmin || $isDelegatedAdmin) {
|
||||
$users = $this->userManager->getDisabledUsers($limit, $offset, $search);
|
||||
$users = array_map(fn (IUser $user): string => $user->getUID(), $users);
|
||||
$users = array_map(fn(IUser $user): string => $user->getUID(), $users);
|
||||
} elseif ($subAdminManager->isSubAdmin($currentUser)) {
|
||||
$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
|
||||
|
||||
|
|
@ -245,10 +251,10 @@ class UsersController extends AUserDataOCSController {
|
|||
$users = array_unique(array_merge(
|
||||
$users,
|
||||
array_map(
|
||||
fn (IUser $user): string => $user->getUID(),
|
||||
fn(IUser $user): string => $user->getUID(),
|
||||
array_filter(
|
||||
$group->searchUsers($search),
|
||||
fn (IUser $user): bool => !$user->isEnabled()
|
||||
fn(IUser $user): bool => !$user->isEnabled()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
|
@ -294,8 +300,9 @@ class UsersController extends AUserDataOCSController {
|
|||
*
|
||||
* 200: Users details returned based on last logged in information
|
||||
*/
|
||||
#[AuthorizedAdminSetting(settings:Users::class)]
|
||||
public function getLastLoggedInUsers(string $search = '',
|
||||
#[AuthorizedAdminSetting(settings: Users::class)]
|
||||
public function getLastLoggedInUsers(
|
||||
string $search = '',
|
||||
?int $limit = null,
|
||||
int $offset = 0,
|
||||
): DataResponse {
|
||||
|
|
@ -355,7 +362,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 400: Invalid location
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function searchByPhoneNumbers(string $location, array $search): DataResponse {
|
||||
public function searchByPhoneNumbers(string $location, array $search): DataResponse
|
||||
{
|
||||
if ($this->phoneNumberUtil->getCountryCodeForRegion($location) === null) {
|
||||
// Not a valid region code
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
|
|
@ -371,7 +379,7 @@ class UsersController extends AUserDataOCSController {
|
|||
foreach ($phoneNumbers as $phone) {
|
||||
$normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $location);
|
||||
if ($normalizedNumber !== null) {
|
||||
$normalizedNumberToKey[$normalizedNumber] = (string)$key;
|
||||
$normalizedNumberToKey[$normalizedNumber] = (string) $key;
|
||||
}
|
||||
|
||||
if ($defaultPhoneRegion !== '' && $defaultPhoneRegion !== $location && str_starts_with($phone, '0')) {
|
||||
|
|
@ -380,7 +388,7 @@ class UsersController extends AUserDataOCSController {
|
|||
// when it's different to the user's given region.
|
||||
$normalizedNumber = $this->phoneNumberUtil->convertToStandardFormat($phone, $defaultPhoneRegion);
|
||||
if ($normalizedNumber !== null) {
|
||||
$normalizedNumberToKey[$normalizedNumber] = (string)$key;
|
||||
$normalizedNumberToKey[$normalizedNumber] = (string) $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -421,7 +429,8 @@ class UsersController extends AUserDataOCSController {
|
|||
/**
|
||||
* @throws OCSException
|
||||
*/
|
||||
private function createNewUserId(): string {
|
||||
private function createNewUserId(): string
|
||||
{
|
||||
$attempts = 0;
|
||||
do {
|
||||
$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
|
||||
|
|
@ -661,7 +670,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: User returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUser(string $userId): DataResponse {
|
||||
public function getUser(string $userId): DataResponse
|
||||
{
|
||||
$includeScopes = false;
|
||||
$currentUser = $this->userSession->getUser();
|
||||
if ($currentUser && $currentUser->getUID() === $userId) {
|
||||
|
|
@ -687,7 +697,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Current user returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getCurrentUser(): DataResponse {
|
||||
public function getCurrentUser(): DataResponse
|
||||
{
|
||||
$user = $this->userSession->getUser();
|
||||
if ($user) {
|
||||
/** @var Provisioning_APIUserDetails $data */
|
||||
|
|
@ -709,7 +720,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Editable fields returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getEditableFields(): DataResponse {
|
||||
public function getEditableFields(): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
if (!$currentLoggedInUser instanceof IUser) {
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
|
|
@ -726,7 +738,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Enabled apps returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getEnabledApps(): DataResponse {
|
||||
public function getEnabledApps(): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
return new DataResponse(['apps' => $this->appManager->getEnabledAppsForUser($currentLoggedInUser)]);
|
||||
}
|
||||
|
|
@ -743,7 +756,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Editable fields for user returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getEditableFieldsForUser(string $userId): DataResponse {
|
||||
public function getEditableFieldsForUser(string $userId): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
if (!$currentLoggedInUser instanceof IUser) {
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
|
|
@ -896,6 +910,220 @@ class UsersController extends AUserDataOCSController {
|
|||
return new DataResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple user account fields atomically.
|
||||
* All submitted fields are validated first; if any fail, no changes are applied.
|
||||
*
|
||||
* Unlike editUser (which updates one field at a time via key/value),
|
||||
* this method accepts named fields and applies them all in a single request.
|
||||
*
|
||||
* @param string $userId The user to update
|
||||
* @param string|null $displayName New display name (null = no change)
|
||||
* @param string|null $password New password (null = no change)
|
||||
* @param string|null $email New primary email (null = no change, '' = clear)
|
||||
* @param string|null $quota New quota e.g. "5 GB" (null = no change)
|
||||
* @param string|null $language Language code e.g. "de" (null = no change)
|
||||
* @param string|null $manager Manager user ID (null = no change, '' = clear)
|
||||
* @param list<string>|null $groups Group IDs to assign (null = no change, [] = remove all)
|
||||
* @param list<string>|null $subadminGroups Subadmin group IDs (null = no change, [] = remove all)
|
||||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
#[UserRateLimit(limit: 50, period: 600)]
|
||||
public function editUserMultiField(
|
||||
string $userId,
|
||||
?string $displayName = null,
|
||||
?string $password = null,
|
||||
?string $email = null,
|
||||
?string $quota = null,
|
||||
?string $language = null,
|
||||
?string $manager = null,
|
||||
?array $groups = null,
|
||||
?array $subadminGroups = null,
|
||||
): DataResponse {
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
if ($currentLoggedInUser === null) {
|
||||
throw new OCSException('', OCSController::RESPOND_UNAUTHORISED);
|
||||
}
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
if ($targetUser === null) {
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isSelf = $targetUser->getUID() === $currentLoggedInUser->getUID();
|
||||
$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
|
||||
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
|
||||
$subAdminManager = $this->groupManager->getSubAdmin();
|
||||
$isSubAdminAccessible = !$isSelf && $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
|
||||
|
||||
$canEditOther = $isAdmin
|
||||
|| ($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin'))
|
||||
|| $isSubAdminAccessible;
|
||||
|
||||
if (!$isSelf && !$canEditOther) {
|
||||
// OCSForbiddenException used here (rather than the older OCSException pattern in editUser)
|
||||
// because it is semantically correct: the caller is authenticated but lacks permission.
|
||||
throw new OCSForbiddenException('Insufficient permissions to edit this user');
|
||||
}
|
||||
|
||||
// Validate all submitted fields — collect errors before applying anything
|
||||
$errors = [];
|
||||
|
||||
if ($displayName !== null) {
|
||||
$backend = $targetUser->getBackend();
|
||||
if ($canEditOther) {
|
||||
$canSetDisplayName = $backend instanceof ISetDisplayNameBackend
|
||||
|| ($backend !== null && $backend->implementsActions(Backend::SET_DISPLAYNAME))
|
||||
|| $targetUser->canChangeDisplayName();
|
||||
} else {
|
||||
$canSetDisplayName = $targetUser->canChangeDisplayName();
|
||||
}
|
||||
if (!$canSetDisplayName) {
|
||||
$errors['displayName'] = $this->l10n->t('Cannot change display name for this user');
|
||||
}
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
if (!$targetUser->canChangePassword()) {
|
||||
$errors['password'] = $this->l10n->t('Password change is not supported by the user backend');
|
||||
} elseif (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
|
||||
$errors['password'] = $this->l10n->t('Password exceeds maximum length');
|
||||
}
|
||||
}
|
||||
|
||||
if ($email !== null && $email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors['email'] = $this->l10n->t('Invalid email address');
|
||||
}
|
||||
|
||||
if ($language !== null) {
|
||||
$availableLanguages = $this->l10nFactory->findAvailableLanguages();
|
||||
if (!in_array($language, $availableLanguages, true) && $language !== 'en') {
|
||||
$errors['language'] = $this->l10n->t('Invalid language');
|
||||
}
|
||||
}
|
||||
|
||||
if ($quota !== null) {
|
||||
if ($quota !== 'none' && $quota !== 'default') {
|
||||
if (is_numeric($quota)) {
|
||||
$quota = (float) $quota;
|
||||
} else {
|
||||
$quota = Util::computerFileSize($quota);
|
||||
}
|
||||
if ($quota === false) {
|
||||
$errors['quota'] = $this->l10n->t('Invalid quota value');
|
||||
} elseif ($quota === -1) {
|
||||
$quota = 'none';
|
||||
} else {
|
||||
$maxQuota = $this->appConfig->getValueInt('files', 'max_quota', -1);
|
||||
if ($maxQuota !== -1 && $quota > $maxQuota) {
|
||||
$errors['quota'] = $this->l10n->t('Invalid quota value. %1$s is exceeding the maximum quota', [$quota]);
|
||||
} else {
|
||||
$quota = Util::humanFileSize($quota);
|
||||
}
|
||||
}
|
||||
}
|
||||
// no else block because quota can be set to 'none' in previous if
|
||||
if ($quota === 'none') {
|
||||
$allowUnlimitedQuota = $this->appConfig->getValueString('files', 'allow_unlimited_quota', '1') === '1';
|
||||
if (!$allowUnlimitedQuota) {
|
||||
$errors['quota'] = $this->l10n->t('Unlimited quota is forbidden on this instance');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($groups !== null && ($isAdmin || $isDelegatedAdmin)) {
|
||||
foreach ($groups as $gid) {
|
||||
if (!$this->groupManager->groupExists($gid)) {
|
||||
$errors['groups'] = $this->l10n->t('Group %s does not exist', [$gid]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($subadminGroups !== null && ($isAdmin || $isDelegatedAdmin)) {
|
||||
foreach ($subadminGroups as $gid) {
|
||||
if (!$this->groupManager->groupExists($gid)) {
|
||||
$errors['subadminGroups'] = $this->l10n->t('Group %s does not exist', [$gid]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return new DataResponse(['errors' => $errors], Http::STATUS_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
// Apply password first — it's the only setter that can fail at runtime
|
||||
// (password policy plugins) after pre-flight validation passes.
|
||||
// If it throws, no other fields have been touched yet.
|
||||
if ($password !== null) {
|
||||
try {
|
||||
$targetUser->setPassword($password);
|
||||
} catch (HintException $e) {
|
||||
return new DataResponse(['errors' => ['password' => $e->getHint()]], Http::STATUS_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remaining changes — all fully validated, setters won't throw
|
||||
if ($displayName !== null) {
|
||||
$targetUser->setDisplayName($displayName);
|
||||
}
|
||||
|
||||
if ($email !== null) {
|
||||
$targetUser->setSystemEMailAddress(mb_strtolower(trim($email)));
|
||||
}
|
||||
|
||||
if ($quota !== null) {
|
||||
$targetUser->setQuota($quota);
|
||||
}
|
||||
|
||||
if ($language !== null) {
|
||||
$forceLanguage = $this->config->getSystemValue('force_language', false);
|
||||
if ($forceLanguage === false || $isAdmin || $isDelegatedAdmin) {
|
||||
$this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $language);
|
||||
}
|
||||
}
|
||||
|
||||
if ($manager !== null) {
|
||||
$targetUser->setManagerUids(array_filter([$manager]));
|
||||
}
|
||||
|
||||
if ($groups !== null && ($isAdmin || $isDelegatedAdmin)) {
|
||||
$currentGroups = $this->groupManager->getUserGroups($targetUser);
|
||||
$currentGroupIds = array_map(fn(IGroup $g) => $g->getGID(), $currentGroups);
|
||||
foreach (array_diff($currentGroupIds, $groups) as $gid) {
|
||||
$this->groupManager->get($gid)?->removeUser($targetUser);
|
||||
}
|
||||
foreach (array_diff($groups, $currentGroupIds) as $gid) {
|
||||
$group = $this->groupManager->get($gid);
|
||||
if ($group === null) {
|
||||
continue;
|
||||
}
|
||||
$group->addUser($targetUser);
|
||||
}
|
||||
}
|
||||
|
||||
if ($subadminGroups !== null && ($isAdmin || $isDelegatedAdmin)) {
|
||||
$currentSubAdminGroups = $subAdminManager->getSubAdminsGroups($targetUser);
|
||||
$currentSubAdminGroupIds = array_map(fn(IGroup $g) => $g->getGID(), $currentSubAdminGroups);
|
||||
foreach (array_diff($currentSubAdminGroupIds, $subadminGroups) as $gid) {
|
||||
$group = $this->groupManager->get($gid);
|
||||
if ($group !== null) {
|
||||
$subAdminManager->deleteSubAdmin($targetUser, $group);
|
||||
}
|
||||
}
|
||||
foreach (array_diff($subadminGroups, $currentSubAdminGroupIds) as $gid) {
|
||||
$group = $this->groupManager->get($gid);
|
||||
if ($group !== null && !$subAdminManager->isSubAdminOfGroup($targetUser, $group)) {
|
||||
$subAdminManager->createSubAdmin($targetUser, $group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DataResponse($this->getUserData($userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoSubAdminRequired
|
||||
*
|
||||
|
|
@ -912,7 +1140,8 @@ class UsersController extends AUserDataOCSController {
|
|||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
#[UserRateLimit(limit: 50, period: 600)]
|
||||
public function editUser(string $userId, string $key, string $value): DataResponse {
|
||||
public function editUser(string $userId, string $key, string $value): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1027,7 +1256,7 @@ class UsersController extends AUserDataOCSController {
|
|||
$quota = $value;
|
||||
if ($quota !== 'none' && $quota !== 'default') {
|
||||
if (is_numeric($quota)) {
|
||||
$quota = (float)$quota;
|
||||
$quota = (float) $quota;
|
||||
} else {
|
||||
$quota = Util::computerFileSize($quota);
|
||||
}
|
||||
|
|
@ -1037,7 +1266,7 @@ class UsersController extends AUserDataOCSController {
|
|||
if ($quota === -1) {
|
||||
$quota = 'none';
|
||||
} else {
|
||||
$maxQuota = (int)$this->config->getAppValue('files', 'max_quota', '-1');
|
||||
$maxQuota = (int) $this->config->getAppValue('files', 'max_quota', '-1');
|
||||
if ($maxQuota !== -1 && $quota > $maxQuota) {
|
||||
throw new OCSException($this->l10n->t('Invalid quota value. %1$s is exceeding the maximum quota', [$value]), 101);
|
||||
}
|
||||
|
|
@ -1089,7 +1318,7 @@ class UsersController extends AUserDataOCSController {
|
|||
$this->config->setUserValue($targetUser->getUID(), 'core', 'timezone', $value);
|
||||
break;
|
||||
case self::USER_FIELD_FIRST_DAY_OF_WEEK:
|
||||
$intValue = (int)$value;
|
||||
$intValue = (int) $value;
|
||||
if ($intValue < -1 || $intValue > 6) {
|
||||
throw new OCSException($this->l10n->t('Invalid first day of week'), 101);
|
||||
}
|
||||
|
|
@ -1236,7 +1465,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function wipeUserDevices(string $userId): DataResponse {
|
||||
public function wipeUserDevices(string $userId): DataResponse
|
||||
{
|
||||
/** @var IUser $currentLoggedInUser */
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
|
||||
|
|
@ -1274,7 +1504,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function deleteUser(string $userId): DataResponse {
|
||||
public function deleteUser(string $userId): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1314,7 +1545,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function disableUser(string $userId): DataResponse {
|
||||
public function disableUser(string $userId): DataResponse
|
||||
{
|
||||
return $this->setEnabled($userId, false);
|
||||
}
|
||||
|
||||
|
|
@ -1329,7 +1561,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function enableUser(string $userId): DataResponse {
|
||||
public function enableUser(string $userId): DataResponse
|
||||
{
|
||||
return $this->setEnabled($userId, true);
|
||||
}
|
||||
|
||||
|
|
@ -1339,7 +1572,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
|
||||
* @throws OCSException
|
||||
*/
|
||||
private function setEnabled(string $userId, bool $value): DataResponse {
|
||||
private function setEnabled(string $userId, bool $value): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1372,7 +1606,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Users groups returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUsersGroups(string $userId): DataResponse {
|
||||
public function getUsersGroups(string $userId): DataResponse
|
||||
{
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1394,7 +1629,7 @@ class UsersController extends AUserDataOCSController {
|
|||
if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
|
||||
// Return the group that the method caller is subadmin of for the user in question
|
||||
$groups = array_values(array_intersect(
|
||||
array_map(static fn (IGroup $group) => $group->getGID(), $subAdminManager->getSubAdminsGroups($loggedInUser)),
|
||||
array_map(static fn(IGroup $group) => $group->getGID(), $subAdminManager->getSubAdminsGroups($loggedInUser)),
|
||||
$this->groupManager->getUserGroupIds($targetUser)
|
||||
));
|
||||
return new DataResponse(['groups' => $groups]);
|
||||
|
|
@ -1417,7 +1652,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Users groups returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUsersGroupsDetails(string $userId): DataResponse {
|
||||
public function getUsersGroupsDetails(string $userId): DataResponse
|
||||
{
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1453,7 +1689,7 @@ class UsersController extends AUserDataOCSController {
|
|||
// Return the group that the method caller is subadmin of for the user in question
|
||||
$gids = array_values(array_intersect(
|
||||
array_map(
|
||||
static fn (IGroup $group) => $group->getGID(),
|
||||
static fn(IGroup $group) => $group->getGID(),
|
||||
$subAdminManager->getSubAdminsGroups($loggedInUser),
|
||||
),
|
||||
$this->groupManager->getUserGroupIds($targetUser)
|
||||
|
|
@ -1494,7 +1730,8 @@ class UsersController extends AUserDataOCSController {
|
|||
* 200: Users subadmin groups returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUserSubAdminGroupsDetails(string $userId): DataResponse {
|
||||
public function getUserSubAdminGroupsDetails(string $userId): DataResponse
|
||||
{
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
@ -1538,7 +1775,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function addToGroup(string $userId, string $groupid = ''): DataResponse {
|
||||
public function addToGroup(string $userId, string $groupid = ''): DataResponse
|
||||
{
|
||||
if ($groupid === '') {
|
||||
throw new OCSException('', 101);
|
||||
}
|
||||
|
|
@ -1578,7 +1816,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function removeFromGroup(string $userId, string $groupid): DataResponse {
|
||||
public function removeFromGroup(string $userId, string $groupid): DataResponse
|
||||
{
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
if ($groupid === null || trim($groupid) === '') {
|
||||
|
|
@ -1643,9 +1882,10 @@ class UsersController extends AUserDataOCSController {
|
|||
*
|
||||
* 200: User added as group subadmin successfully
|
||||
*/
|
||||
#[AuthorizedAdminSetting(settings:Users::class)]
|
||||
#[AuthorizedAdminSetting(settings: Users::class)]
|
||||
#[PasswordConfirmationRequired]
|
||||
public function addSubAdmin(string $userId, string $groupid): DataResponse {
|
||||
public function addSubAdmin(string $userId, string $groupid): DataResponse
|
||||
{
|
||||
$group = $this->groupManager->get($groupid);
|
||||
$user = $this->userManager->get($userId);
|
||||
|
||||
|
|
@ -1683,9 +1923,10 @@ class UsersController extends AUserDataOCSController {
|
|||
*
|
||||
* 200: User removed as group subadmin successfully
|
||||
*/
|
||||
#[AuthorizedAdminSetting(settings:Users::class)]
|
||||
#[AuthorizedAdminSetting(settings: Users::class)]
|
||||
#[PasswordConfirmationRequired]
|
||||
public function removeSubAdmin(string $userId, string $groupid): DataResponse {
|
||||
public function removeSubAdmin(string $userId, string $groupid): DataResponse
|
||||
{
|
||||
$group = $this->groupManager->get($groupid);
|
||||
$user = $this->userManager->get($userId);
|
||||
$subAdminManager = $this->groupManager->getSubAdmin();
|
||||
|
|
@ -1717,8 +1958,9 @@ class UsersController extends AUserDataOCSController {
|
|||
*
|
||||
* 200: User subadmin groups returned
|
||||
*/
|
||||
#[AuthorizedAdminSetting(settings:Users::class)]
|
||||
public function getUserSubAdminGroups(string $userId): DataResponse {
|
||||
#[AuthorizedAdminSetting(settings: Users::class)]
|
||||
public function getUserSubAdminGroups(string $userId): DataResponse
|
||||
{
|
||||
$groups = $this->getUserSubAdminGroupsData($userId);
|
||||
return new DataResponse($groups);
|
||||
}
|
||||
|
|
@ -1734,7 +1976,8 @@ class UsersController extends AUserDataOCSController {
|
|||
*/
|
||||
#[PasswordConfirmationRequired]
|
||||
#[NoAdminRequired]
|
||||
public function resendWelcomeMessage(string $userId): DataResponse {
|
||||
public function resendWelcomeMessage(string $userId): DataResponse
|
||||
{
|
||||
$currentLoggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use OCP\Accounts\IAccountManager;
|
|||
use OCP\Accounts\IAccountProperty;
|
||||
use OCP\Accounts\IAccountPropertyCollection;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCS\OCSException;
|
||||
|
|
@ -68,6 +69,7 @@ class UsersControllerTest extends TestCase {
|
|||
private IRootFolder $rootFolder;
|
||||
private IPhoneNumberUtil $phoneNumberUtil;
|
||||
private IAppManager $appManager;
|
||||
private IAppConfig&MockObject $appConfig;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -89,6 +91,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
|
||||
$this->phoneNumberUtil = new PhoneNumberUtil();
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
$this->appConfig = $this->createMock(IAppConfig::class);
|
||||
$this->rootFolder = $this->createMock(IRootFolder::class);
|
||||
|
||||
$l10n = $this->createMock(IL10N::class);
|
||||
|
|
@ -116,6 +119,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->eventDispatcher,
|
||||
$this->phoneNumberUtil,
|
||||
$this->appManager,
|
||||
$this->appConfig,
|
||||
])
|
||||
->onlyMethods(['fillStorageInfo'])
|
||||
->getMock();
|
||||
|
|
@ -505,6 +509,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->eventDispatcher,
|
||||
$this->phoneNumberUtil,
|
||||
$this->appManager,
|
||||
$this->appConfig,
|
||||
])
|
||||
->onlyMethods(['editUser'])
|
||||
->getMock();
|
||||
|
|
@ -2571,6 +2576,7 @@ class UsersControllerTest extends TestCase {
|
|||
$targetUser = $this->createMock(IUser::class);
|
||||
$targetUser->method('getUID')->willReturn('targetuser');
|
||||
$targetUser->method('canChangeDisplayName')->willReturn(true);
|
||||
$targetUser->method('getBackend')->willReturn($this->createMock(UserInterface::class));
|
||||
$this->userManager->method('get')->with('targetuser')->willReturn($targetUser);
|
||||
|
||||
$this->groupManager->method('isAdmin')->with('admin')->willReturn(true);
|
||||
|
|
@ -2580,14 +2586,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->groupManager->method('getSubAdmin')->willReturn($subAdmin);
|
||||
|
||||
$targetUser->expects($this->once())->method('setDisplayName')->with('New Name')->willReturn(true);
|
||||
|
||||
$account = $this->createMock(\OCP\Accounts\IAccount::class);
|
||||
$emailProp = $this->createMock(IAccountProperty::class);
|
||||
// Current email is empty — ensures the update path is triggered (not skipped as unchanged)
|
||||
$emailProp->method('getValue')->willReturn('');
|
||||
$account->method('getProperty')->willReturn($emailProp);
|
||||
$this->accountManager->method('getAccount')->willReturn($account);
|
||||
$this->accountManager->expects($this->once())->method('updateAccount')->with($account);
|
||||
$targetUser->expects($this->once())->method('setSystemEMailAddress')->with('new@example.com');
|
||||
|
||||
$result = $this->api->editUserMultiField('targetuser', displayName: 'New Name', email: 'new@example.com');
|
||||
|
||||
|
|
@ -2628,6 +2627,7 @@ class UsersControllerTest extends TestCase {
|
|||
|
||||
$targetUser = $this->createMock(IUser::class);
|
||||
$targetUser->method('getUID')->willReturn('targetuser');
|
||||
$targetUser->method('getBackend')->willReturn($this->createMock(UserInterface::class));
|
||||
$this->userManager->method('get')->with('targetuser')->willReturn($targetUser);
|
||||
|
||||
$this->groupManager->method('isAdmin')->with('admin')->willReturn(true);
|
||||
|
|
@ -2684,6 +2684,7 @@ class UsersControllerTest extends TestCase {
|
|||
|
||||
$targetUser = $this->createMock(IUser::class);
|
||||
$targetUser->method('getUID')->willReturn('targetuser');
|
||||
$targetUser->method('getBackend')->willReturn($this->createMock(UserInterface::class));
|
||||
$this->userManager->method('get')->with('targetuser')->willReturn($targetUser);
|
||||
|
||||
$this->groupManager->method('isAdmin')->with('admin')->willReturn(true);
|
||||
|
|
@ -2697,6 +2698,7 @@ class UsersControllerTest extends TestCase {
|
|||
$newGroup->method('getGID')->willReturn('newgroup');
|
||||
|
||||
$this->groupManager->method('getUserGroups')->willReturn([$oldGroup]);
|
||||
$this->groupManager->method('groupExists')->willReturn(true);
|
||||
$this->groupManager->method('get')->willReturnMap([
|
||||
['newgroup', $newGroup],
|
||||
['oldgroup', $oldGroup],
|
||||
|
|
@ -3994,6 +3996,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->eventDispatcher,
|
||||
$this->phoneNumberUtil,
|
||||
$this->appManager,
|
||||
$this->appConfig,
|
||||
])
|
||||
->onlyMethods(['getUserData'])
|
||||
->getMock();
|
||||
|
|
@ -4088,6 +4091,7 @@ class UsersControllerTest extends TestCase {
|
|||
$this->eventDispatcher,
|
||||
$this->phoneNumberUtil,
|
||||
$this->appManager,
|
||||
$this->appConfig,
|
||||
])
|
||||
->onlyMethods(['getUserData'])
|
||||
->getMock();
|
||||
|
|
|
|||
|
|
@ -215,6 +215,21 @@ const mutations = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply multiple updated fields to a user in the local store.
|
||||
*
|
||||
* @param {object} state Store state
|
||||
* @param {object} options destructuring object
|
||||
* @param {string} options.userid User id
|
||||
* @param {object} options.data Updated user data from server
|
||||
*/
|
||||
editUserMultiField(state, { userid, data }) {
|
||||
const index = state.users.findIndex((user) => user.id === userid)
|
||||
if (index !== -1) {
|
||||
state.users.splice(index, 1, { ...state.users[index], ...data })
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset users list
|
||||
*
|
||||
|
|
@ -783,6 +798,29 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update multiple user fields atomically via the new bulk endpoint.
|
||||
*
|
||||
* @param {object} context store context
|
||||
* @param {object} options destructuring object
|
||||
* @param {string} options.userid User id
|
||||
* @param {object} options.payload Changed fields to send
|
||||
* @return {Promise}
|
||||
*/
|
||||
async editUserMultiField(context, { userid, payload }) {
|
||||
try {
|
||||
await api.requireAdmin()
|
||||
const response = await api.patch(
|
||||
generateOcsUrl('cloud/users/{userid}', { userid }),
|
||||
payload,
|
||||
)
|
||||
context.commit('editUserMultiField', { userid, data: response.data.ocs.data })
|
||||
} catch (error) {
|
||||
context.commit('API_FAILURE', { userid, error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send welcome mail
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue