diff --git a/app/Auth/Queries/AllUsersPaginatedAndSorted.php b/app/Auth/Queries/AllUsersPaginatedAndSorted.php new file mode 100644 index 000000000..664db1925 --- /dev/null +++ b/app/Auth/Queries/AllUsersPaginatedAndSorted.php @@ -0,0 +1,42 @@ +select(['*']) + ->scopes(['withLastActivityAt']) + ->with(['roles', 'avatar']) + ->withCount('mfaValues') + ->orderBy($sort, $sortData['order']); + + if ($sortData['search']) { + $term = '%' . $sortData['search'] . '%'; + $query->where(function ($query) use ($term) { + $query->where('name', 'like', $term) + ->orWhere('email', 'like', $term); + }); + } + + return $query->paginate($count); + } + +} \ No newline at end of file diff --git a/app/Auth/Queries/UserContentCounts.php b/app/Auth/Queries/UserContentCounts.php new file mode 100644 index 000000000..b8d99be14 --- /dev/null +++ b/app/Auth/Queries/UserContentCounts.php @@ -0,0 +1,30 @@ + $user->id]; + + return [ + 'pages' => Page::visible()->where($createdBy)->count(), + 'chapters' => Chapter::visible()->where($createdBy)->count(), + 'books' => Book::visible()->where($createdBy)->count(), + 'shelves' => Bookshelf::visible()->where($createdBy)->count(), + ]; + } +} \ No newline at end of file diff --git a/app/Auth/Queries/UserRecentlyCreatedContent.php b/app/Auth/Queries/UserRecentlyCreatedContent.php new file mode 100644 index 000000000..31dd9de68 --- /dev/null +++ b/app/Auth/Queries/UserRecentlyCreatedContent.php @@ -0,0 +1,37 @@ +orderBy('created_at', 'desc') + ->where('created_by', '=', $user->id) + ->take($count) + ->get(); + }; + + return [ + 'pages' => $query(Page::visible()->where('draft', '=', false)), + 'chapters' => $query(Chapter::visible()), + 'books' => $query(Book::visible()), + 'shelves' => $query(Bookshelf::visible()), + ]; + } +} \ No newline at end of file diff --git a/app/Auth/User.php b/app/Auth/User.php index b7f88b590..4e2183244 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -77,17 +77,15 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * This holds the user's permissions when loaded. - * - * @var ?Collection */ - protected $permissions; + protected ?Collection $permissions; /** * This holds the default user when loaded. * * @var null|User */ - protected static $defaultUser = null; + protected static ?User $defaultUser = null; /** * Returns the default public user. diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 606fd5d65..28ce96c49 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -5,26 +5,18 @@ namespace BookStack\Auth; use BookStack\Actions\ActivityType; use BookStack\Auth\Access\UserInviteService; use BookStack\Entities\EntityProvider; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; -use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\UserUpdateException; use BookStack\Facades\Activity; use BookStack\Uploads\UserAvatars; use Exception; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; class UserRepo { - protected $userAvatar; - protected $inviteService; + protected UserAvatars $userAvatar; + protected UserInviteService $inviteService; /** * UserRepo constructor. @@ -59,104 +51,6 @@ class UserRepo return User::query()->where('slug', '=', $slug)->firstOrFail(); } - /** - * Get all users as Builder for API. - */ - public function getApiUsersBuilder(): Builder - { - return User::query()->select(['*']) - ->scopes('withLastActivityAt') - ->with(['avatar']); - } - - /** - * Get all the users with their permissions in a paginated format. - * Note: Due to the use of email search this should only be used when - * user is assumed to be trusted. (Admin users). - * Email search can be abused to extract email addresses. - */ - public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator - { - $sort = $sortData['sort']; - - $query = User::query()->select(['*']) - ->scopes(['withLastActivityAt']) - ->with(['roles', 'avatar']) - ->withCount('mfaValues') - ->orderBy($sort, $sortData['order']); - - if ($sortData['search']) { - $term = '%' . $sortData['search'] . '%'; - $query->where(function ($query) use ($term) { - $query->where('name', 'like', $term) - ->orWhere('email', 'like', $term); - }); - } - - return $query->paginate($count); - } - - /** - * Assign a user to a system-level role. - * - * @throws NotFoundException - */ - public function attachSystemRole(User $user, string $systemRoleName) - { - $role = Role::getSystemRole($systemRoleName); - if (is_null($role)) { - throw new NotFoundException("Role '{$systemRoleName}' not found"); - } - $user->attachRole($role); - } - - /** - * Checks if the give user is the only admin. - */ - public function isOnlyAdmin(User $user): bool - { - if (!$user->hasSystemRole('admin')) { - return false; - } - - $adminRole = Role::getSystemRole('admin'); - if ($adminRole->users()->count() > 1) { - return false; - } - - return true; - } - - /** - * Set the assigned user roles via an array of role IDs. - * - * @throws UserUpdateException - */ - public function setUserRoles(User $user, array $roles) - { - if ($this->demotingLastAdmin($user, $roles)) { - throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl()); - } - - $user->roles()->sync($roles); - } - - /** - * Check if the given user is the last admin and their new roles no longer - * contains the admin role. - */ - protected function demotingLastAdmin(User $user, array $newRoles): bool - { - if ($this->isOnlyAdmin($user)) { - $adminRole = Role::getSystemRole('admin'); - if (!in_array(strval($adminRole->id), $newRoles)) { - return true; - } - } - - return false; - } - /** * Create a new basic instance of user with the given pre-validated data. * @@ -299,54 +193,11 @@ class UserRepo } } - /** - * Get the recently created content for this given user. - */ - public function getRecentlyCreated(User $user, int $count = 20): array - { - $query = function (Builder $query) use ($user, $count) { - return $query->orderBy('created_at', 'desc') - ->where('created_by', '=', $user->id) - ->take($count) - ->get(); - }; - - return [ - 'pages' => $query(Page::visible()->where('draft', '=', false)), - 'chapters' => $query(Chapter::visible()), - 'books' => $query(Book::visible()), - 'shelves' => $query(Bookshelf::visible()), - ]; - } - - /** - * Get asset created counts for the give user. - */ - public function getAssetCounts(User $user): array - { - $createdBy = ['created_by' => $user->id]; - - return [ - 'pages' => Page::visible()->where($createdBy)->count(), - 'chapters' => Chapter::visible()->where($createdBy)->count(), - 'books' => Book::visible()->where($createdBy)->count(), - 'shelves' => Bookshelf::visible()->where($createdBy)->count(), - ]; - } - - /** - * Get the roles in the system that are assignable to a user. - */ - public function getAllRoles(): Collection - { - return Role::query()->orderBy('display_name', 'asc')->get(); - } - /** * Get an avatar image for a user and set it as their avatar. * Returns early if avatars disabled or not set in config. */ - public function downloadAndAssignUserAvatar(User $user): void + protected function downloadAndAssignUserAvatar(User $user): void { try { $this->userAvatar->fetchAndAssignToUser($user); @@ -354,4 +205,51 @@ class UserRepo Log::error('Failed to save user avatar image'); } } + + /** + * Checks if the give user is the only admin. + */ + protected function isOnlyAdmin(User $user): bool + { + if (!$user->hasSystemRole('admin')) { + return false; + } + + $adminRole = Role::getSystemRole('admin'); + if ($adminRole->users()->count() > 1) { + return false; + } + + return true; + } + + /** + * Set the assigned user roles via an array of role IDs. + * + * @throws UserUpdateException + */ + protected function setUserRoles(User $user, array $roles) + { + if ($this->demotingLastAdmin($user, $roles)) { + throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl()); + } + + $user->roles()->sync($roles); + } + + /** + * Check if the given user is the last admin and their new roles no longer + * contains the admin role. + */ + protected function demotingLastAdmin(User $user, array $newRoles): bool + { + if ($this->isOnlyAdmin($user)) { + $adminRole = Role::getSystemRole('admin'); + if (!in_array(strval($adminRole->id), $newRoles)) { + return true; + } + } + + return false; + } } diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php index c571d383e..e4660314d 100644 --- a/app/Console/Commands/CreateAdmin.php +++ b/app/Console/Commands/CreateAdmin.php @@ -2,6 +2,7 @@ namespace BookStack\Console\Commands; +use BookStack\Auth\Role; use BookStack\Auth\UserRepo; use BookStack\Exceptions\NotFoundException; use Illuminate\Console\Command; @@ -85,7 +86,7 @@ class CreateAdmin extends Command } $user = $this->userRepo->createWithoutActivity($validator->validated()); - $this->userRepo->attachSystemRole($user, 'admin'); + $user->attachRole(Role::getSystemRole('admin')); $user->email_confirmed = true; $user->save(); diff --git a/app/Http/Controllers/Api/UserApiController.php b/app/Http/Controllers/Api/UserApiController.php index d58904938..4f0d30034 100644 --- a/app/Http/Controllers/Api/UserApiController.php +++ b/app/Http/Controllers/Api/UserApiController.php @@ -72,7 +72,9 @@ class UserApiController extends ApiController */ public function list() { - $users = $this->userRepo->getApiUsersBuilder(); + $users = User::query()->select(['*']) + ->scopes('withLastActivityAt') + ->with(['avatar']); return $this->apiListingResponse($users, [ 'id', 'name', 'slug', 'email', 'external_auth_id', diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a635bbaa6..a22b0394a 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,6 +3,8 @@ namespace BookStack\Http\Controllers; use BookStack\Auth\Access\SocialAuthService; +use BookStack\Auth\Queries\AllUsersPaginatedAndSorted; +use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Auth\UserRepo; use BookStack\Exceptions\ImageUploadException; @@ -39,12 +41,16 @@ class UserController extends Controller 'search' => $request->get('search', ''), 'sort' => $request->get('sort', 'name'), ]; - $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails); + + $users = (new AllUsersPaginatedAndSorted())->run(20, $listDetails); $this->setPageTitle(trans('settings.users')); $users->appends($listDetails); - return view('users.index', ['users' => $users, 'listDetails' => $listDetails]); + return view('users.index', [ + 'users' => $users, + 'listDetails' => $listDetails + ]); } /** @@ -54,7 +60,7 @@ class UserController extends Controller { $this->checkPermission('users-manage'); $authMethod = config('auth.method'); - $roles = $this->userRepo->getAllRoles(); + $roles = Role::query()->orderBy('display_name', 'asc')->get(); $this->setPageTitle(trans('settings.users_add_new')); return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]); @@ -109,7 +115,7 @@ class UserController extends Controller $activeSocialDrivers = $socialAuthService->getActiveDrivers(); $mfaMethods = $user->mfaValues->groupBy('method'); $this->setPageTitle(trans('settings.user_profile')); - $roles = $this->userRepo->getAllRoles(); + $roles = Role::query()->orderBy('display_name', 'asc')->get(); return view('users.edit', [ 'user' => $user, diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 5fd8f7b88..e75aced12 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -3,6 +3,8 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityQueries; +use BookStack\Auth\Queries\UserContentCounts; +use BookStack\Auth\Queries\UserRecentlyCreatedContent; use BookStack\Auth\UserRepo; class UserProfileController extends Controller @@ -15,8 +17,8 @@ class UserProfileController extends Controller $user = $repo->getBySlug($slug); $userActivity = $activities->userActivity($user); - $recentlyCreated = $repo->getRecentlyCreated($user, 5); - $assetCounts = $repo->getAssetCounts($user); + $recentlyCreated = (new UserRecentlyCreatedContent())->run($user, 5); + $assetCounts = (new UserContentCounts())->run($user); $this->setPageTitle($user->name);