diff --git a/app/Authorization/FlatAuthorization.php b/app/Authorization/FlatAuthorization.php new file mode 100644 index 00000000..48b4ad73 --- /dev/null +++ b/app/Authorization/FlatAuthorization.php @@ -0,0 +1,76 @@ +getPermissionID($permission); + + if (!is_numeric($permissionId)) { + return false; + } + + if ( + $this->permissionModel->doesGroupHavePermission( + $groupId, + (int) $permissionId + ) + ) { + return true; + } + + return false; + } + + /** + * Makes a member a part of multiple groups. + * + * @param $user_id + * @param array|null $groups // Either collection of ID or names + * + * @return bool + */ + public function setUserGroups(int $user_id, $groups) + { + if (empty($user_id) || !is_numeric($user_id)) { + return null; + } + + // remove user from all groups before resetting it in new groups + $this->groupModel->removeUserFromAllGroups($user_id); + + if (empty($groups)) { + return true; + } + + foreach ($groups as $group) { + $this->addUserToGroup($user_id, $group); + } + + return true; + } +} diff --git a/app/Authorization/GroupModel.php b/app/Authorization/GroupModel.php new file mode 100644 index 00000000..597a54f1 --- /dev/null +++ b/app/Authorization/GroupModel.php @@ -0,0 +1,18 @@ +select('auth_groups.*') + ->like('name', 'podcast_', 'after') + ->findAll(); + } + + public function getUserRoles() + { + return $this->select('auth_groups.*') + ->notLike('name', 'podcast_', 'after') + ->findAll(); + } +} diff --git a/app/Authorization/PermissionModel.php b/app/Authorization/PermissionModel.php new file mode 100644 index 00000000..b8b56274 --- /dev/null +++ b/app/Authorization/PermissionModel.php @@ -0,0 +1,63 @@ +getPermissionsForGroup($groupId); + + return count($groupPerms) && + array_key_exists($permissionId, $groupPerms); + } + + /** + * Gets all permissions for a group in a way that can be + * easily used to check against: + * + * [ + * id => name, + * id => name + * ] + * + * @param int $groupId + * + * @return array + */ + public function getPermissionsForGroup(int $groupId): array + { + if (!($found = cache("group{$groupId}_permissions"))) { + $groupPermissions = $this->db + ->table('auth_groups_permissions') + ->select('id, auth_permissions.name') + ->join( + 'auth_permissions', + 'auth_permissions.id = permission_id', + 'inner' + ) + ->where('group_id', $groupId) + ->get() + ->getResultObject(); + + $found = []; + foreach ($groupPermissions as $row) { + $found[$row->id] = strtolower($row->name); + } + + cache()->save("group{$groupId}_permissions", $found, 300); + } + + return $found; + } +} diff --git a/app/Config/Filters.php b/app/Config/Filters.php index fef58ff5..da69ca64 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -12,7 +12,7 @@ class Filters extends BaseConfig 'honeypot' => \CodeIgniter\Filters\Honeypot::class, 'login' => \Myth\Auth\Filters\LoginFilter::class, 'role' => \Myth\Auth\Filters\RoleFilter::class, - 'permission' => \Myth\Auth\Filters\PermissionFilter::class, + 'permission' => \App\Filters\Permission::class, ]; // Always applied before every request diff --git a/app/Config/Paths.php b/app/Config/Paths.php index 9c126be1..f574dad5 100644 --- a/app/Config/Paths.php +++ b/app/Config/Paths.php @@ -21,7 +21,7 @@ class Paths * as this file. */ public $systemDirectory = - __DIR__ . '/../../vendor/codeigniter4/framework/system'; + __DIR__ . '/../../vendor/codeigniter4/codeigniter4/system'; /* *--------------------------------------------------------------- diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 552f33a6..84a96218 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -22,6 +22,13 @@ $routes->setDefaultMethod('index'); $routes->setTranslateURIDashes(false); $routes->set404Override(); $routes->setAutoRoute(false); + +/** + * -------------------------------------------------------------------- + * Placeholder definitions + * -------------------------------------------------------------------- + */ + $routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}'); $routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}'); $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}'); @@ -64,9 +71,11 @@ $routes->group( 'as' => 'admin_home', ]); + $routes->get('my-podcasts', 'Podcast::myPodcasts', [ + 'as' => 'my_podcasts', + ]); $routes->get('podcasts', 'Podcast::list', [ 'as' => 'podcast_list', - 'filter' => 'permission:podcasts-list', ]); $routes->get('new-podcast', 'Podcast::create', [ 'as' => 'podcast_create', @@ -80,58 +89,97 @@ $routes->group( $routes->group('podcasts/(:num)', function ($routes) { $routes->get('/', 'Podcast::view/$1', [ 'as' => 'podcast_view', + 'filter' => 'permission:podcasts-view,podcast-view', ]); $routes->get('edit', 'Podcast::edit/$1', [ 'as' => 'podcast_edit', + 'filter' => 'permission:podcasts-edit,podcast-edit', + ]); + $routes->post('edit', 'Podcast::attemptEdit/$1', [ + 'filter' => 'permission:podcasts-edit,podcast-edit', ]); - $routes->post('edit', 'Podcast::attemptEdit/$1'); $routes->add('delete', 'Podcast::delete/$1', [ 'as' => 'podcast_delete', + 'filter' => 'permission:podcasts-edit,podcast-delete', ]); // Podcast episodes $routes->get('episodes', 'Episode::list/$1', [ 'as' => 'episode_list', + 'filter' => 'permission:podcasts-view,podcast-view', ]); $routes->get('new-episode', 'Episode::create/$1', [ 'as' => 'episode_create', + 'filter' => + 'permission:episodes-create,podcast_episodes-create', + ]); + $routes->post('new-episode', 'Episode::attemptCreate/$1', [ + 'filter' => + 'permission:episodes-create,podcast_episodes-create', ]); - $routes->post('new-episode', 'Episode::attemptCreate/$1'); $routes->get('episodes/(:num)', 'Episode::view/$1/$2', [ 'as' => 'episode_view', + 'filter' => 'permission:episodes-list,podcast_episodes-list', ]); $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [ 'as' => 'episode_edit', + 'filter' => 'permission:episodes-edit,podcast_episodes-edit', ]); - $routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2'); + $routes->post( + 'episodes/(:num)/edit', + 'Episode::attemptEdit/$1/$2', + [ + 'filter' => + 'permission:episodes-edit,podcast_episodes-edit', + ] + ); $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [ 'as' => 'episode_delete', + 'filter' => + 'permission:episodes-delete,podcast_episodes-delete', ]); // Podcast contributors $routes->get('contributors', 'Contributor::list/$1', [ 'as' => 'contributor_list', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', ]); $routes->get('add-contributor', 'Contributor::add/$1', [ 'as' => 'contributor_add', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ]); + $routes->post('add-contributor', 'Contributor::attemptAdd/$1', [ + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', ]); - $routes->post('add-contributor', 'Contributor::attemptAdd/$1'); $routes->get( 'contributors/(:num)/edit', 'Contributor::edit/$1/$2', [ 'as' => 'contributor_edit', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', ] ); $routes->post( 'contributors/(:num)/edit', - 'Contributor::attemptEdit/$1/$2' + 'Contributor::attemptEdit/$1/$2', + [ + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ] ); $routes->add( 'contributors/(:num)/remove', 'Contributor::remove/$1/$2', - ['as' => 'contributor_remove'] + [ + 'as' => 'contributor_remove', + 'filter' => + 'permission:podcasts-manage_contributors,podcast-manage_contributors', + ] ); }); @@ -147,6 +195,13 @@ $routes->group( $routes->post('new-user', 'User::attemptCreate', [ 'filter' => 'permission:users-create', ]); + $routes->get('users/(:num)/edit', 'User::edit/$1', [ + 'as' => 'user_edit', + 'filter' => 'permission:users-manage_authorizations', + ]); + $routes->post('users/(:num)/edit', 'User::attemptEdit/$1', [ + 'filter' => 'permission:users-manage_authorizations', + ]); $routes->add('users/(:num)/ban', 'User::ban/$1', [ 'as' => 'user_ban', @@ -194,40 +249,44 @@ $routes->group( /** * Overwriting Myth:auth routes file */ -$routes->group(config('App')->authGateway, function ($routes) { - // Login/out - $routes->get('login', 'Auth::login', ['as' => 'login']); - $routes->post('login', 'Auth::attemptLogin'); - $routes->get('logout', 'Auth::logout', ['as' => 'logout']); +$routes->group( + config('App')->authGateway, + ['namespace' => 'Myth\Auth\Controllers'], + function ($routes) { + // Login/out + $routes->get('login', 'AuthController::login', ['as' => 'login']); + $routes->post('login', 'AuthController::attemptLogin'); + $routes->get('logout', 'AuthController::logout', ['as' => 'logout']); - // Registration - $routes->get('register', 'Auth::register', [ - 'as' => 'register', - ]); - $routes->post('register', 'Auth::attemptRegister'); + // Registration + $routes->get('register', 'AuthController::register', [ + 'as' => 'register', + ]); + $routes->post('register', 'AuthController::attemptRegister'); - // Activation - $routes->get('activate-account', 'Auth::activateAccount', [ - 'as' => 'activate-account', - ]); - $routes->get('resend-activate-account', 'Auth::resendActivateAccount', [ - 'as' => 'resend-activate-account', - ]); + // Activation + $routes->get('activate-account', 'AuthController::activateAccount', [ + 'as' => 'activate-account', + ]); + $routes->get( + 'resend-activate-account', + 'AuthController::resendActivateAccount', + [ + 'as' => 'resend-activate-account', + ] + ); - // Forgot/Resets - $routes->get('forgot', 'Auth::forgotPassword', [ - 'as' => 'forgot', - ]); - $routes->post('forgot', 'Auth::attemptForgot'); - $routes->get('reset-password', 'Auth::resetPassword', [ - 'as' => 'reset-password', - ]); - $routes->post('reset-password', 'Auth::attemptReset'); - $routes->get('change-password', 'Auth::changePassword', [ - 'as' => 'change_pass', - ]); - $routes->post('change-password', 'Auth::attemptChange'); -}); + // Forgot/Resets + $routes->get('forgot', 'AuthController::forgotPassword', [ + 'as' => 'forgot', + ]); + $routes->post('forgot', 'Auth::attemptForgot'); + $routes->get('reset-password', 'AuthController::resetPassword', [ + 'as' => 'reset-password', + ]); + $routes->post('reset-password', 'AuthController::attemptReset'); + } +); /** * -------------------------------------------------------------------- diff --git a/app/Config/Services.php b/app/Config/Services.php index 2fe37be6..634c5460 100644 --- a/app/Config/Services.php +++ b/app/Config/Services.php @@ -1,6 +1,12 @@ authenticationLibs[$lib]; + + $instance = new $class($config); + + if (empty($userModel)) { + $userModel = new UserModel(); + } + + if (empty($loginModel)) { + $loginModel = new LoginModel(); + } + + return $instance->setUserModel($userModel)->setLoginModel($loginModel); + } + + public static function authorization( + Model $groupModel = null, + Model $permissionModel = null, + Model $userModel = null, + bool $getShared = true + ) { + if ($getShared) { + return self::getSharedInstance( + 'authorization', + $groupModel, + $permissionModel, + $userModel + ); + } + + if (is_null($groupModel)) { + $groupModel = new GroupModel(); + } + + if (is_null($permissionModel)) { + $permissionModel = new PermissionModel(); + } + + $instance = new FlatAuthorization($groupModel, $permissionModel); + + if (is_null($userModel)) { + $userModel = new UserModel(); + } + + return $instance->setUserModel($userModel); + } } diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php index 1cf39aee..708912f7 100644 --- a/app/Controllers/Admin/Contributor.php +++ b/app/Controllers/Admin/Contributor.php @@ -7,43 +7,25 @@ namespace App\Controllers\Admin; +use App\Authorization\GroupModel; use App\Models\PodcastModel; -use Myth\Auth\Authorization\GroupModel; -use Myth\Auth\Config\Services; -use Myth\Auth\Models\UserModel; +use App\Models\UserModel; class Contributor extends BaseController { protected \App\Entities\Podcast $podcast; - protected ?\Myth\Auth\Entities\User $user; + protected ?\App\Entities\User $user; public function _remap($method, ...$params) { - if ( - !has_permission('podcasts-manage_contributors') || - !has_permission("podcasts:$params[0]-manage_contributors") - ) { - throw new \RuntimeException(lang('Auth.notEnoughPrivilege')); - } - - $podcast_model = new PodcastModel(); - - $this->podcast = $podcast_model->find($params[0]); + $this->podcast = (new PodcastModel())->find($params[0]); if (count($params) > 1) { - $user_model = new UserModel(); if ( - !($this->user = $user_model - ->select('users.*') - ->join( - 'users_podcasts', - 'users_podcasts.user_id = users.id' - ) - ->where([ - 'users.id' => $params[1], - 'podcast_id' => $params[0], - ]) - ->first()) + !($this->user = (new UserModel())->getPodcastContributor( + $params[1], + $params[0] + )) ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } @@ -63,18 +45,10 @@ class Contributor extends BaseController public function add() { - $user_model = new UserModel(); - $group_model = new GroupModel(); - - $roles = $group_model - ->select('auth_groups.*') - ->like('name', 'podcasts:' . $this->podcast->id, 'after') - ->findAll(); - $data = [ 'podcast' => $this->podcast, - 'users' => $user_model->findAll(), - 'roles' => $roles, + 'users' => (new UserModel())->findAll(), + 'roles' => (new GroupModel())->getContributorRoles(), ]; echo view('admin/contributor/add', $data); @@ -82,46 +56,32 @@ class Contributor extends BaseController public function attemptAdd() { - $authorize = Services::authorization(); - - $user_id = (int) $this->request->getPost('user'); - $group_id = (int) $this->request->getPost('role'); - - // Add user to chosen group - $authorize->addUserToGroup($user_id, $group_id); - - (new PodcastModel())->addContributorToPodcast( - $user_id, - $this->podcast->id - ); + try { + (new PodcastModel())->addPodcastContributor( + $this->request->getPost('user'), + $this->podcast->id, + $this->request->getPost('role') + ); + } catch (\Exception $e) { + return redirect() + ->back() + ->withInput() + ->with('errors', [lang('Contributor.alreadyAddedError')]); + } return redirect()->route('contributor_list', [$this->podcast->id]); } public function edit() { - $group_model = new GroupModel(); - - $roles = $group_model - ->select('auth_groups.*') - ->like('name', 'podcasts:' . $this->podcast->id, 'after') - ->findAll(); - - $user_role = $group_model - ->select('auth_groups.*') - ->join( - 'auth_groups_users', - 'auth_groups_users.group_id = auth_groups.id' - ) - ->where('auth_groups_users.user_id', $this->user->id) - ->like('name', 'podcasts:' . $this->podcast->id, 'after') - ->first(); - $data = [ 'podcast' => $this->podcast, 'user' => $this->user, - 'user_role' => $user_role, - 'roles' => $roles, + 'contributor_group_id' => (new PodcastModel())->getContributorGroupId( + $this->user->id, + $this->podcast->id + ), + 'roles' => (new GroupModel())->getContributorRoles(), ]; echo view('admin/contributor/edit', $data); @@ -129,28 +89,10 @@ class Contributor extends BaseController public function attemptEdit() { - $authorize = Services::authorization(); - - $group_model = new GroupModel(); - - $group = $group_model - ->select('auth_groups.*') - ->join( - 'auth_groups_users', - 'auth_groups_users.group_id = auth_groups.id' - ) - ->where('user_id', $this->user->id) - ->like('name', 'podcasts:' . $this->podcast->id, 'after') - ->first(); - - $authorize->removeUserFromGroup( - (int) $this->user->id, - (int) $group->id - ); - - $authorize->addUserToGroup( - (int) $this->user->id, - (int) $this->request->getPost('role') + (new PodcastModel())->updatePodcastContributor( + $this->user->id, + $this->podcast->id, + $this->request->getPost('role') ); return redirect()->route('contributor_list', [$this->podcast->id]); @@ -158,30 +100,34 @@ class Contributor extends BaseController public function remove() { - $authorize = Services::authorization(); + if ($this->podcast->owner_id == $this->user->id) { + return redirect() + ->back() + ->with('errors', [ + lang('Contributor.removeOwnerContributorError'), + ]); + } - $group_model = new GroupModel(); - - $group = $group_model - ->select('auth_groups.*') - ->join( - 'auth_groups_users', - 'auth_groups_users.group_id = auth_groups.id' + $podcast_model = new PodcastModel(); + if ( + !$podcast_model->removePodcastContributor( + $this->user->id, + $this->podcast->id ) - ->like('name', 'podcasts:' . $this->podcast->id, 'after') - ->where('user_id', $this->user->id) - ->first(); + ) { + return redirect() + ->back() + ->with('errors', $podcast_model->errors()); + } - $authorize->removeUserFromGroup( - (int) $this->user->id, - (int) $group->id - ); - - (new PodcastModel())->removeContributorFromPodcast( - $this->user->id, - $this->podcast->id - ); - - return redirect()->route('contributor_list', [$this->podcast->id]); + return redirect() + ->back() + ->with( + 'message', + lang('Contributor.removeContributorSuccess', [ + 'username' => $this->user->username, + 'podcastTitle' => $this->podcast->title, + ]) + ); } } diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index 70964117..74efa436 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -17,39 +17,7 @@ class Episode extends BaseController public function _remap($method, ...$params) { - switch ($method) { - case 'list': - if ( - !has_permission('episodes-list') || - !has_permission("podcasts:$params[0]:episodes-list") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - case 'edit': - if ( - !has_permission('episodes-edit') || - !has_permission("podcasts:$params[0]:episodes-edit") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - case 'delete': - if ( - !has_permission('episodes-delete') || - !has_permission("podcasts:$params[0]:episodes-delete") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - } - - $podcast_model = new PodcastModel(); - - $this->podcast = $podcast_model->find($params[0]); + $this->podcast = (new PodcastModel())->find($params[0]); if (count($params) > 1) { $episode_model = new EpisodeModel(); diff --git a/app/Controllers/Admin/Myaccount.php b/app/Controllers/Admin/Myaccount.php index 5cd23535..4f3f7571 100644 --- a/app/Controllers/Admin/Myaccount.php +++ b/app/Controllers/Admin/Myaccount.php @@ -7,8 +7,8 @@ namespace App\Controllers\Admin; -use Myth\Auth\Config\Services; -use Myth\Auth\Models\UserModel; +use Config\Services; +use App\Models\UserModel; class Myaccount extends BaseController { diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 8259902f..4b99340c 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -9,6 +9,7 @@ namespace App\Controllers\Admin; use App\Models\CategoryModel; use App\Models\LanguageModel; use App\Models\PodcastModel; +use Config\Services; class Podcast extends BaseController { @@ -17,38 +18,7 @@ class Podcast extends BaseController public function _remap($method, ...$params) { if (count($params) > 0) { - switch ($method) { - case 'view': - if ( - !has_permission('podcasts-view') || - !has_permission("podcasts:$params[0]-view") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - case 'edit': - if ( - !has_permission('podcasts-edit') || - !has_permission("podcasts:$params[0]-edit") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - case 'delete': - if ( - !has_permission('podcasts-delete') || - !has_permission("podcasts:$params[0]-delete") - ) { - throw new \RuntimeException( - lang('Auth.notEnoughPrivilege') - ); - } - } - - $podcast_model = new PodcastModel(); - if (!($this->podcast = $podcast_model->find($params[0]))) { + if (!($this->podcast = (new PodcastModel())->find($params[0]))) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } } @@ -56,18 +26,22 @@ class Podcast extends BaseController return $this->$method(); } + public function myPodcasts() + { + $data = [ + 'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id), + ]; + + return view('admin/podcast/list', $data); + } + public function list() { - $podcast_model = new PodcastModel(); - - $all_podcasts = []; - if (has_permission('podcasts-list')) { - $all_podcasts = $podcast_model->findAll(); - } else { - $all_podcasts = $podcast_model->getUserPodcasts(user()->id); + if (!has_permission('podcasts-list')) { + return redirect()->route('my_podcasts'); } - $data = ['all_podcasts' => $all_podcasts]; + $data = ['all_podcasts' => (new PodcastModel())->findAll()]; return view('admin/podcast/list', $data); } @@ -145,7 +119,14 @@ class Podcast extends BaseController ->with('errors', $podcast_model->errors()); } - $podcast_model->addContributorToPodcast(user()->id, $new_podcast_id); + $authorize = Services::authorization(); + $podcast_admin_group = $authorize->group('podcast_admin'); + + $podcast_model->addPodcastContributor( + user()->id, + $new_podcast_id, + $podcast_admin_group->id + ); $db->transComplete(); diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php index 4a8342f5..f958fe2e 100644 --- a/app/Controllers/Admin/User.php +++ b/app/Controllers/Admin/User.php @@ -7,11 +7,13 @@ namespace App\Controllers\Admin; -use Myth\Auth\Models\UserModel; +use App\Authorization\GroupModel; +use App\Models\UserModel; +use Config\Services; class User extends BaseController { - protected ?\Myth\Auth\Entities\User $user; + protected ?\App\Entities\User $user; public function _remap($method, ...$params) { @@ -27,16 +29,18 @@ class User extends BaseController public function list() { - $user_model = new UserModel(); - - $data = ['all_users' => $user_model->findAll()]; + $data = ['all_users' => (new UserModel())->findAll()]; return view('admin/user/list', $data); } public function create() { - echo view('admin/user/create'); + $data = [ + 'roles' => (new GroupModel())->getUserRoles(), + ]; + + echo view('admin/user/create', $data); } public function attemptCreate() @@ -62,14 +66,13 @@ class User extends BaseController } // Save the user - $user = new \Myth\Auth\Entities\User($this->request->getPost()); + $user = new \App\Entities\User($this->request->getPost()); // Activate user $user->activate(); // Force user to reset his password on first connection - $user->force_pass_reset = true; - $user->generateResetHash(); + $user->forcePasswordReset(); if (!$user_model->save($user)) { return redirect() @@ -81,15 +84,46 @@ class User extends BaseController // Success! return redirect() ->route('user_list') - ->with('message', lang('User.createSuccess')); + ->with( + 'message', + lang('User.createSuccess', [ + 'username' => $user->username, + ]) + ); + } + + public function edit() + { + $data = [ + 'user' => $this->user, + 'roles' => (new GroupModel())->getUserRoles(), + ]; + + echo view('admin/user/edit', $data); + } + + public function attemptEdit() + { + $authorize = Services::authorization(); + + $roles = $this->request->getPost('roles'); + $authorize->setUserGroups($this->user->id, $roles); + + // Success! + return redirect() + ->route('user_list') + ->with( + 'message', + lang('User.rolesEditSuccess', [ + 'username' => $this->user->username, + ]) + ); } public function forcePassReset() { $user_model = new UserModel(); - - $this->user->force_pass_reset = true; - $this->user->generateResetHash(); + $this->user->forcePasswordReset(); if (!$user_model->save($this->user)) { return redirect() @@ -100,12 +134,29 @@ class User extends BaseController // Success! return redirect() ->route('user_list') - ->with('message', lang('User.forcePassResetSuccess')); + ->with( + 'message', + lang('User.forcePassResetSuccess', [ + 'username' => $this->user->username, + ]) + ); } public function ban() { + $authorize = Services::authorization(); + if ($authorize->inGroup('superadmin', $this->user->id)) { + return redirect() + ->back() + ->with('errors', [ + lang('User.banSuperAdminError', [ + 'username' => $this->user->username, + ]), + ]); + } + $user_model = new UserModel(); + // TODO: add ban reason? $this->user->ban(''); if (!$user_model->save($this->user)) { @@ -116,7 +167,12 @@ class User extends BaseController return redirect() ->route('user_list') - ->with('message', lang('User.banSuccess')); + ->with( + 'message', + lang('User.banSuccess', [ + 'username' => $this->user->username, + ]) + ); } public function unBan() @@ -132,16 +188,37 @@ class User extends BaseController return redirect() ->route('user_list') - ->with('message', lang('User.unbanSuccess')); + ->with( + 'message', + lang('User.unbanSuccess', [ + 'username' => $this->user->username, + ]) + ); } public function delete() { + $authorize = Services::authorization(); + if ($authorize->inGroup('superadmin', $this->user->id)) { + return redirect() + ->back() + ->with('errors', [ + lang('User.deleteSuperAdminError', [ + 'username' => $this->user->username, + ]), + ]); + } + $user_model = new UserModel(); $user_model->delete($this->user->id); return redirect() - ->route('user_list') - ->with('message', lang('User.deleteSuccess')); + ->back() + ->with( + 'message', + lang('User.deleteSuccess', [ + 'username' => $this->user->username, + ]) + ); } } diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php deleted file mode 100644 index f9190065..00000000 --- a/app/Controllers/Auth.php +++ /dev/null @@ -1,98 +0,0 @@ - $this->config, - 'email' => user()->email, - 'token' => user()->reset_hash, - ]); - } - - public function attemptChange() - { - $users = new UserModel(); - - // First things first - log the reset attempt. - $users->logResetAttempt( - $this->request->getPost('email'), - $this->request->getPost('token'), - $this->request->getIPAddress(), - (string) $this->request->getUserAgent() - ); - - $rules = [ - 'token' => 'required', - 'email' => 'required|valid_email', - 'password' => 'required|strong_password', - 'pass_confirm' => 'required|matches[password]', - ]; - - if (!$this->validate($rules)) { - return redirect() - ->back() - ->withInput() - ->with('errors', $users->errors()); - } - - $user = $users - ->where('email', $this->request->getPost('email')) - ->where('reset_hash', $this->request->getPost('token')) - ->first(); - - if (is_null($user)) { - return redirect() - ->back() - ->with('error', lang('Auth.forgotNoUser')); - } - - // Reset token still valid? - if ( - !empty($user->reset_expires) && - time() > $user->reset_expires->getTimestamp() - ) { - return redirect() - ->back() - ->withInput() - ->with('error', lang('Auth.resetTokenExpired')); - } - - // Success! Save the new password, and cleanup the reset hash. - $user->password = $this->request->getPost('password'); - $user->reset_hash = null; - $user->reset_at = date('Y-m-d H:i:s'); - $user->reset_expires = null; - $user->force_pass_reset = false; - $users->save($user); - - return redirect() - ->route('login') - ->with('message', lang('Auth.resetSuccess')); - } -} diff --git a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php index abed46d5..5b731576 100644 --- a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php +++ b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php @@ -27,10 +27,16 @@ class AddUsersPodcasts extends Migration 'constraint' => 20, 'unsigned' => true, ], + 'group_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + ], ]); $this->forge->addPrimaryKey(['user_id', 'podcast_id']); $this->forge->addForeignKey('user_id', 'users', 'id'); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->addForeignKey('group_id', 'auth_groups', 'id'); $this->forge->createTable('users_podcasts'); } diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index af0eeab0..1c60c427 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -14,141 +14,267 @@ use CodeIgniter\Database\Seeder; class AuthSeeder extends Seeder { + protected $groups = [ + [ + 'name' => 'superadmin', + 'description' => + 'Somebody who has access to all the castopod instance features', + ], + [ + 'name' => 'podcast_admin', + 'description' => + 'Somebody who has access to all the features within a given podcast', + ], + ]; + + /** Build permissions array as a list of: + * + * ``` + * context => [ + * [action, description], + * [action, description], + * ... + * ] + * ``` + */ + protected $permissions = [ + 'users' => [ + [ + 'name' => 'create', + 'description' => 'Create a user', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'list', + 'description' => 'List all users', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage_authorizations', + 'description' => 'Add or remove roles/permissions to a user', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage_bans', + 'description' => 'Ban / unban a user', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'force_pass_reset', + 'description' => + 'Force a user to update his password upon next login', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete', + 'description' => + 'Delete user without removing him from database', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => + 'Delete all occurrences of a user from the database', + 'has_permission' => ['superadmin'], + ], + ], + 'podcasts' => [ + [ + 'name' => 'create', + 'description' => 'Add a new podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'list', + 'description' => 'List all podcasts and their episodes', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'view', + 'description' => 'View any podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'edit', + 'description' => 'Edit any podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage_contributors', + 'description' => 'Add / remove contributors to a podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage_publication', + 'description' => 'Publish / unpublish a podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete', + 'description' => + 'Delete a podcast without removing it from database', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete any podcast from the database', + 'has_permission' => ['superadmin'], + ], + ], + 'episodes' => [ + [ + 'name' => 'list', + 'description' => 'List all episodes of any podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'create', + 'description' => 'Add a new episode to any podcast', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'edit', + 'description' => 'Edit any podcast episode', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'manage_publications', + 'description' => 'Publish / unpublish any podcast episode', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete', + 'description' => + 'Delete any podcast episode without removing it from database', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete any podcast episode from database', + 'has_permission' => ['superadmin'], + ], + ], + 'podcast' => [ + [ + 'name' => 'view', + 'description' => 'View a podcast', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'edit', + 'description' => 'Edit a podcast', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'delete', + 'description' => + 'Delete a podcast without removing it from the database', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete a podcast from the database', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'manage_contributors', + 'description' => + 'Add / remove contributors to a podcast and edit their roles', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'manage_publication', + 'description' => 'Publish / unpublish a podcast', + 'has_permission' => ['podcast_admin'], + ], + ], + 'podcast_episodes' => [ + [ + 'name' => 'list', + 'description' => 'List all episodes of a podcast', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'create', + 'description' => 'Add new episodes for a podcast', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'edit', + 'description' => 'Edit an episode of a podcast', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'delete', + 'description' => + 'Delete an episode of a podcast without removing it from the database', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => + 'Delete all occurrences of an episode of a podcast from the database', + 'has_permission' => ['podcast_admin'], + ], + [ + 'name' => 'manage_publications', + 'description' => 'Publish / unpublish episodes of a podcast', + 'has_permission' => ['podcast_admin'], + ], + ], + ]; + + static function getGroupIdByName($name, $data_groups) + { + foreach ($data_groups as $group) { + if ($group['name'] === $name) { + return $group['id']; + } + } + return null; + } + public function run() { - helper('auth'); - - $groups = [['id' => 1, 'name' => 'superadmin', 'description' => '']]; - - /** Build permissions array as a list of: - * - * ``` - * context => [ - * [action, description], - * [action, description], - * ... - * ] - * ``` - */ - $permissions = [ - 'users' => [ - ['name' => 'create', 'description' => 'Create a user'], - ['name' => 'list', 'description' => 'List all users'], - [ - 'name' => 'manage_authorizations', - 'description' => - 'Add or remove roles/permissions to a user', - ], - [ - 'name' => 'manage_bans', - 'description' => 'Ban / unban a user', - ], - [ - 'name' => 'force_pass_reset', - 'description' => - 'Force a user to update his password upon next login', - ], - [ - 'name' => 'delete', - 'description' => - 'Delete user without removing him from database', - ], - [ - 'name' => 'delete_permanently', - 'description' => - 'Delete all occurrences of a user from the database', - ], - ], - 'podcasts' => [ - ['name' => 'create', 'description' => 'Add a new podcast'], - [ - 'name' => 'list', - 'description' => 'List all podcasts and their episodes', - ], - ['name' => 'view', 'description' => 'View any podcast'], - ['name' => 'edit', 'description' => 'Edit any podcast'], - [ - 'name' => 'manage_contributors', - 'description' => 'Add / remove contributors to a podcast', - ], - [ - 'name' => 'manage_publication', - 'description' => 'Publish / unpublish a podcast', - ], - [ - 'name' => 'delete', - 'description' => - 'Delete a podcast without removing it from database', - ], - [ - 'name' => 'delete_permanently', - 'description' => 'Delete any podcast from the database', - ], - ], - 'episodes' => [ - [ - 'name' => 'list', - 'description' => 'List all episodes of any podcast', - ], - [ - 'name' => 'create', - 'description' => 'Add a new episode to any podcast', - ], - ['name' => 'edit', 'description' => 'Edit any podcast episode'], - [ - 'name' => 'manage_publications', - 'description' => 'Publish / unpublish any podcast episode', - ], - [ - 'name' => 'delete', - 'description' => - 'Delete any podcast episode without removing it from database', - ], - [ - 'name' => 'delete_permanently', - 'description' => 'Delete any podcast episode from database', - ], - ], - ]; + $group_id = 0; + $data_groups = []; + foreach ($this->groups as $group) { + array_push($data_groups, [ + 'id' => ++$group_id, + 'name' => $group['name'], + 'description' => $group['description'], + ]); + } // Map permissions to a format the `auth_permissions` table expects $data_permissions = []; $data_groups_permissions = []; $permission_id = 0; - foreach ($permissions as $context => $actions) { + foreach ($this->permissions as $context => $actions) { foreach ($actions as $action) { array_push($data_permissions, [ 'id' => ++$permission_id, - 'name' => get_permission($context, $action['name']), + 'name' => $context . '-' . $action['name'], 'description' => $action['description'], ]); - // add all permissions to superadmin - array_push($data_groups_permissions, [ - 'group_id' => 1, - 'permission_id' => $permission_id, - ]); + foreach ($action['has_permission'] as $role) { + // link permission to specified groups + array_push($data_groups_permissions, [ + 'group_id' => $this->getGroupIdByName( + $role, + $data_groups + ), + 'permission_id' => $permission_id, + ]); + } } } $this->db->table('auth_permissions')->insertBatch($data_permissions); - $this->db->table('auth_groups')->insertBatch($groups); + $this->db->table('auth_groups')->insertBatch($data_groups); $this->db ->table('auth_groups_permissions') ->insertBatch($data_groups_permissions); - - // TODO: Remove superadmin user as it is used for testing purposes - $this->db->table('users')->insert([ - 'id' => 1, - 'username' => 'admin', - 'email' => 'admin@castopod.com', - 'password_hash' => - // password: AGUehL3P - '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6', - 'active' => 1, - ]); - $this->db - ->table('auth_groups_users') - ->insert(['group_id' => 1, 'user_id' => 1]); } } diff --git a/app/Database/Seeds/TestSeeder.php b/app/Database/Seeds/TestSeeder.php new file mode 100644 index 00000000..efdbd5b8 --- /dev/null +++ b/app/Database/Seeds/TestSeeder.php @@ -0,0 +1,35 @@ +db->table('users')->insert([ + 'id' => 1, + 'username' => 'admin', + 'email' => 'admin@castopod.com', + 'password_hash' => + '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6', + 'active' => 1, + ]); + $this->db + ->table('auth_groups_users') + ->insert(['group_id' => 1, 'user_id' => 1]); + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index b4b281ce..a8268f32 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -9,7 +9,6 @@ namespace App\Entities; use App\Models\PodcastModel; use CodeIgniter\Entity; -use League\CommonMark\CommonMarkConverter; use Parsedown; class Episode extends Entity diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 991d1e44..9a86b1d0 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -9,7 +9,7 @@ namespace App\Entities; use App\Models\EpisodeModel; use CodeIgniter\Entity; -use Myth\Auth\Models\UserModel; +use App\Models\UserModel; use Parsedown; class Podcast extends Entity @@ -19,7 +19,7 @@ class Podcast extends Entity protected string $image_media_path; protected string $image_url; protected $episodes; - protected \Myth\Auth\Entities\User $owner; + protected \App\Entities\User $owner; protected $contributors; protected string $description_html; @@ -110,7 +110,7 @@ class Podcast extends Entity /** * Returns the podcast owner * - * @return \Myth\Auth\Entities\User + * @return \App\Entities\User */ public function getOwner() { @@ -127,7 +127,7 @@ class Podcast extends Entity return $this->owner; } - public function setOwner(\Myth\Auth\Entities\User $user) + public function setOwner(\App\Entities\User $user) { $this->attributes['owner_id'] = $user->id; @@ -137,15 +137,23 @@ class Podcast extends Entity /** * Returns all podcast contributors * - * @return \Myth\Auth\Entities\User[] + * @return \App\Entities\User[] */ public function getContributors() { - return (new UserModel()) - ->select('users.*') - ->join('users_podcasts', 'users_podcasts.user_id = users.id') - ->where('users_podcasts.podcast_id', $this->attributes['id']) - ->findAll(); + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcasts must be created before getting contributors.' + ); + } + + if (empty($this->contributors)) { + $this->contributors = (new UserModel())->getPodcastContributors( + $this->id + ); + } + + return $this->contributors; } public function getDescriptionHtml() diff --git a/app/Entities/User.php b/app/Entities/User.php new file mode 100644 index 00000000..3277fdcb --- /dev/null +++ b/app/Entities/User.php @@ -0,0 +1,42 @@ + 'boolean', + 'force_pass_reset' => 'boolean', + 'podcast_role' => '?string', + ]; + + /** + * Returns the podcasts the user is contributing to + * + * @return \App\Entities\Podcast[] + */ + public function getPodcasts() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Users must be created before getting podcasts.' + ); + } + + if (empty($this->podcasts)) { + $this->podcasts = (new PodcastModel())->getUserPodcasts($this->id); + } + + return $this->podcasts; + } +} diff --git a/app/Filters/Permission.php b/app/Filters/Permission.php new file mode 100644 index 00000000..a83d9662 --- /dev/null +++ b/app/Filters/Permission.php @@ -0,0 +1,115 @@ +check()) { + session()->set('redirect_url', current_url()); + return redirect('login'); + } + + helper('misc'); + $authorize = Services::authorization(); + $router = Services::router(); + $routerParams = $router->params(); + $result = false; + + // Check if user has at least one of the permissions + foreach ($params as $permission) { + // check if permission is for a specific podcast + if ( + (startsWith($permission, 'podcast-') || + startsWith($permission, 'podcast_episodes-')) && + count($routerParams) > 0 + ) { + if ( + $group_id = (new PodcastModel())->getContributorGroupId( + $authenticate->id(), + $routerParams[0] + ) + ) { + if ( + $authorize->groupHasPermission($permission, $group_id) + ) { + $result = true; + break; + } + } + } elseif ( + $authorize->hasPermission($permission, $authenticate->id()) + ) { + $result = true; + break; + } + } + + if (!$result) { + if ($authenticate->silent()) { + $redirectURL = session('redirect_url') ?? '/'; + unset($_SESSION['redirect_url']); + return redirect() + ->to($redirectURL) + ->with('error', lang('Auth.notEnoughPrivilege')); + } else { + throw new PermissionException(lang('Auth.notEnoughPrivilege')); + } + } + } + + //-------------------------------------------------------------------- + + /** + * Allows After filters to inspect and modify the response + * object as needed. This method does not allow any way + * to stop execution of other after filters, short of + * throwing an Exception or Error. + * + * @param \CodeIgniter\HTTP\RequestInterface $request + * @param \CodeIgniter\HTTP\ResponseInterface $response + * @param array|null $arguments + * + * @return void + */ + public function after( + RequestInterface $request, + ResponseInterface $response, + $arguments = null + ) { + } + + //-------------------------------------------------------------------- +} diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php deleted file mode 100644 index 344e0636..00000000 --- a/app/Helpers/auth_helper.php +++ /dev/null @@ -1,19 +0,0 @@ - 'Podcasts', 'users' => 'Users', 'admin_home' => 'Home', + 'my_podcasts' => 'My podcasts', 'podcast_list' => 'All podcasts', 'podcast_create' => 'New podcast', 'user_list' => 'All users', diff --git a/app/Language/en/Contributor.php b/app/Language/en/Contributor.php index 552c9f64..57a8448a 100644 --- a/app/Language/en/Contributor.php +++ b/app/Language/en/Contributor.php @@ -6,6 +6,9 @@ */ return [ + 'removeOwnerContributorError' => 'You can\'t remove the podcast owner!', + 'removeContributorSuccess' => 'You have successfully removed {username} from {podcastTitle}', + 'alreadyAddedError' => 'The contributor you\'re trying to add has already been added!', 'podcast_contributors' => 'Podcast contributors', 'add' => 'Add contributor', 'add_contributor' => 'Add a contributor for {0}', diff --git a/app/Language/en/User.php b/app/Language/en/User.php index c9e0fc03..aa5491c0 100644 --- a/app/Language/en/User.php +++ b/app/Language/en/User.php @@ -6,11 +6,15 @@ */ return [ - 'createSuccess' => 'User created successfully! The new user will be prompted with a password reset during his first login attempt.', - 'forcePassResetSuccess' => 'The user will be prompted with a password reset during his next login attempt.', - 'banSuccess' => 'User has been banned.', - 'unbanSuccess' => 'User has been unbanned.', - 'deleteSuccess' => 'User has been deleted.', + 'createSuccess' => 'User created successfully! {username} will be prompted with a password reset upon first authentication.', + 'rolesEditSuccess' => '{username}\'s roles have been successfully updated.', + 'forcePassResetSuccess' => '{username} will be prompted with a password reset upon next visit.', + 'banSuccess' => '{username} has been banned.', + 'unbanSuccess' => '{username} has been unbanned.', + 'banSuperAdminError' => '{username} is a superadmin, one does not simply ban a superadmin…', + 'deleteSuperAdminError' => '{username} is a superadmin, one does not simply delete a superadmin…', + 'deleteSuccess' => '{username} has been deleted.', + 'edit_roles' => 'Edit {username}\'s roles', 'forcePassReset' => 'Force pass reset', 'ban' => 'Ban', 'unban' => 'Unban', @@ -24,6 +28,7 @@ return [ 'new_password' => 'New Password', 'repeat_password' => 'Repeat password', 'repeat_new_password' => 'Repeat new password', + 'roles' => 'Roles', 'submit_create' => 'Create user', 'submit_edit' => 'Save', ] diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 86d47c3a..ece640d9 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -8,8 +8,6 @@ namespace App\Models; use CodeIgniter\Model; -use Myth\Auth\Authorization\GroupModel; -use Myth\Auth\Config\Services; class PodcastModel extends Model { @@ -58,7 +56,7 @@ class PodcastModel extends Model ]; protected $validationMessages = []; - protected $afterInsert = ['clearCache', 'createPodcastPermissions']; + protected $afterInsert = ['clearCache']; protected $afterUpdate = ['clearCache']; protected $beforeDelete = ['clearCache']; @@ -77,17 +75,29 @@ class PodcastModel extends Model ->findAll(); } - public function addContributorToPodcast($user_id, $podcast_id) + public function addPodcastContributor($user_id, $podcast_id, $group_id) { $data = [ 'user_id' => (int) $user_id, 'podcast_id' => (int) $podcast_id, + 'group_id' => (int) $group_id, ]; return $this->db->table('users_podcasts')->insert($data); } - public function removeContributorFromPodcast($user_id, $podcast_id) + public function updatePodcastContributor($user_id, $podcast_id, $group_id) + { + return $this->db + ->table('users_podcasts') + ->where([ + 'user_id' => (int) $user_id, + 'podcast_id' => (int) $podcast_id, + ]) + ->update(['group_id' => $group_id]); + } + + public function removePodcastContributor($user_id, $podcast_id) { return $this->db ->table('users_podcasts') @@ -98,6 +108,24 @@ class PodcastModel extends Model ->delete(); } + public function getContributorGroupId($user_id, $podcast_id) + { + // TODO: return only the group id + $user_podcast = $this->db + ->table('users_podcasts') + ->select('group_id') + ->where([ + 'user_id' => $user_id, + 'podcast_id' => $podcast_id, + ]) + ->get() + ->getResultObject(); + + return (int) count($user_podcast) > 0 + ? $user_podcast[0]->group_id + : false; + } + protected function clearCache(array $data) { $podcast = $this->find( @@ -109,101 +137,11 @@ class PodcastModel extends Model cache()->delete(md5($podcast->link)); // TODO: clear cache for every podcast's episode page? // foreach ($podcast->episodes as $episode) { - // $cache->delete(md5($episode->link)); + // cache()->delete(md5($episode->link)); // } $data['podcast'] = $podcast; return $data; } - - protected function createPodcastPermissions(array $data) - { - $authorize = Services::authorization(); - - $podcast = $data['podcast']; - - $podcast_permissions = [ - 'podcasts:' . $podcast->id => [ - [ - 'name' => 'View', - 'description' => "View the $podcast->name podcast", - ], - [ - 'name' => 'edit', - 'description' => "Edit the $podcast->name podcast", - ], - [ - 'name' => 'delete', - 'description' => "Delete the $podcast->name podcast without removing it from the database", - ], - [ - 'name' => 'delete_permanently', - 'description' => "Delete the $podcast->name podcast from the database", - ], - [ - 'name' => 'manage_contributors', - 'description' => "Add / remove contributors to the $podcast->name podcast and edit their roles", - ], - [ - 'name' => 'manage_publication', - 'description' => "Publish / unpublish $podcast->name", - ], - ], - 'podcasts:' . $podcast->id . ':episodes' => [ - [ - 'name' => 'list', - 'description' => "List all episodes of the $podcast->name podcast", - ], - [ - 'name' => 'create', - 'description' => "Add new episodes for the $podcast->name podcast", - ], - [ - 'name' => 'edit', - 'description' => "Edit an episode of the $podcast->name podcast", - ], - [ - 'name' => 'delete', - 'description' => "Delete an episode of the $podcast->name podcast without removing it from the database", - ], - [ - 'name' => 'delete_permanently', - 'description' => "Delete all occurrences of an episode of the $podcast->name podcast from the database", - ], - [ - 'name' => 'manage_publications', - 'description' => "Publish / unpublish episodes of the $podcast->name podcast", - ], - ], - ]; - - $group_model = new GroupModel(); - $owner_group_id = $group_model->insert( - [ - 'name' => "podcasts:$podcast->id" . '_owner', - 'description' => "The owner of the $podcast->name podcast", - ], - true - ); - - // add podcast owner to owner group - $authorize->addUserToGroup($podcast->owner_id, $owner_group_id); - - foreach ($podcast_permissions as $context => $actions) { - foreach ($actions as $action) { - $permission_id = $authorize->createPermission( - get_permission($context, $action['name']), - $action['description'] - ); - - $authorize->addPermissionToGroup( - $permission_id, - $owner_group_id - ); - } - } - - return $data; - } } diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php new file mode 100644 index 00000000..876cc947 --- /dev/null +++ b/app/Models/UserModel.php @@ -0,0 +1,28 @@ +select('users.*, auth_groups.name as podcast_role') + ->join('users_podcasts', 'users_podcasts.user_id = users.id') + ->join('auth_groups', 'auth_groups.id = users_podcasts.group_id') + ->where('users_podcasts.podcast_id', $podcast_id) + ->findAll(); + } + + public function getPodcastContributor($user_id, $podcast_id) + { + return $this->select('users.*') + ->join('users_podcasts', 'users_podcasts.user_id = users.id') + ->where([ + 'users.id' => $user_id, + 'podcast_id' => $podcast_id, + ]) + ->first(); + } +} diff --git a/app/Views/admin/_partials/_podcast-card.php b/app/Views/admin/_partials/_podcast-card.php index 4f769df7..a56c7fe6 100644 --- a/app/Views/admin/_partials/_podcast-card.php +++ b/app/Views/admin/_partials/_podcast-card.php @@ -16,7 +16,7 @@ ) ?>" data-toggle="tooltip" data-placement="bottom" title=""> - Username - Permissions + Role Actions @@ -27,10 +27,7 @@ contributors as $contributor): ?> username ?> - [permissions - ) ?>] + podcast_role ?> + + + isBanned() ? 'Yes' : 'No' ?> - diff --git a/app/Views/auth/change_password.php b/app/Views/auth/change_password.php deleted file mode 100644 index 3ca545b6..00000000 --- a/app/Views/auth/change_password.php +++ /dev/null @@ -1,29 +0,0 @@ -extend($config->viewLayout) ?> - -section('title') ?> - -endSection() ?> - - -section('content') ?> - -
- - - - - - - - - - - - -
- -endSection() ?> diff --git a/composer.json b/composer.json index 1a3749ef..ae4625de 100644 --- a/composer.json +++ b/composer.json @@ -6,12 +6,12 @@ "license": "AGPL-3.0-or-later", "require": { "php": ">=7.2", - "codeigniter4/framework": "4.0.3", "james-heinrich/getid3": "~2.0.0-dev", "whichbrowser/parser": "^2.0", "geoip2/geoip2": "~2.0", - "myth/auth": "1.0-beta.2", - "erusev/parsedown": "^1.7" + "myth/auth": "dev-develop", + "erusev/parsedown": "^1.7", + "codeigniter4/codeigniter4": "dev-develop" }, "require-dev": { "mikey179/vfsstream": "1.6.*", @@ -32,5 +32,13 @@ "forum": "http://forum.codeigniter.com/", "source": "https://github.com/codeigniter4/CodeIgniter4", "slack": "https://codeigniterchat.slack.com" - } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/codeigniter4/codeigniter4" + } + ] } diff --git a/composer.lock b/composer.lock index 752e638c..4b63d07e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3656eaed72238d7b46af985ec86f6533", + "content-hash": "b483083efa09cc772800d5df7d339d02", "packages": [ { - "name": "codeigniter4/framework", - "version": "v4.0.3", + "name": "codeigniter4/codeigniter4", + "version": "dev-develop", "source": { "type": "git", - "url": "https://github.com/codeigniter4/framework.git", - "reference": "edd88b18483e309bab1411651d846aace255ab36" + "url": "https://github.com/codeigniter4/CodeIgniter4.git", + "reference": "81c08a5ddd70d2243c0842dfcc22c93dd044bc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/framework/zipball/edd88b18483e309bab1411651d846aace255ab36", - "reference": "edd88b18483e309bab1411651d846aace255ab36", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/81c08a5ddd70d2243c0842dfcc22c93dd044bc42", + "reference": "81c08a5ddd70d2243c0842dfcc22c93dd044bc42", "shasum": "" }, "require": { @@ -32,8 +32,10 @@ }, "require-dev": { "codeigniter4/codeigniter4-standard": "^1.0", + "fzaninotto/faker": "^1.9@dev", "mikey179/vfsstream": "1.6.*", "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", "squizlabs/php_codesniffer": "^3.3" }, "type": "project", @@ -42,13 +44,28 @@ "CodeIgniter\\": "system/" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "post-update-cmd": [ + "@composer dump-autoload", + "CodeIgniter\\ComposerScripts::postUpdate", + "bash admin/setup.sh" + ], + "test": [ + "phpunit" + ] + }, "license": [ "MIT" ], "description": "The CodeIgniter framework v4", "homepage": "https://codeigniter.com", - "time": "2020-05-01T05:01:20+00:00" + "support": { + "forum": "http://forum.codeigniter.com/", + "source": "https://github.com/codeigniter4/CodeIgniter4", + "slack": "https://codeigniterchat.slack.com", + "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" + }, + "time": "2020-07-29T03:07:26+00:00" }, { "name": "composer/ca-bundle", @@ -217,16 +234,16 @@ }, { "name": "james-heinrich/getid3", - "version": "2.0.x-dev", + "version": "v2.0.0-beta3", "source": { "type": "git", "url": "https://github.com/JamesHeinrich/getID3.git", - "reference": "8cf765ec4c42ed732993a9aa60b638ee398df154" + "reference": "5515a2d24667c3c0ff49fdcbdadc405c0880c7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/8cf765ec4c42ed732993a9aa60b638ee398df154", - "reference": "8cf765ec4c42ed732993a9aa60b638ee398df154", + "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/5515a2d24667c3c0ff49fdcbdadc405c0880c7a2", + "reference": "5515a2d24667c3c0ff49fdcbdadc405c0880c7a2", "shasum": "" }, "require": { @@ -294,7 +311,7 @@ "tags", "video" ], - "time": "2019-07-22T12:33:16+00:00" + "time": "2020-07-21T08:15:44+00:00" }, { "name": "kint-php/kint", @@ -581,16 +598,16 @@ }, { "name": "myth/auth", - "version": "1.0-beta.2", + "version": "dev-develop", "source": { "type": "git", "url": "https://github.com/lonnieezell/myth-auth.git", - "reference": "b110088785ba22a82264e1df444621f3e1618f95" + "reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/b110088785ba22a82264e1df444621f3e1618f95", - "reference": "b110088785ba22a82264e1df444621f3e1618f95", + "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694", + "reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694", "shasum": "" }, "require": { @@ -600,7 +617,7 @@ "codeigniter4/codeigniter4": "dev-develop", "fzaninotto/faker": "^1.9@dev", "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "8.5.*" }, "type": "library", "autoload": { @@ -627,7 +644,17 @@ "authorization", "codeigniter" ], - "time": "2019-12-12T05:12:25+00:00" + "funding": [ + { + "url": "https://github.com/lonnieezell", + "type": "github" + }, + { + "url": "https://www.patreon.com/lonnieezell", + "type": "patreon" + } + ], + "time": "2020-07-16T14:00:14+00:00" }, { "name": "psr/cache", @@ -1106,28 +1133,27 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.1.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + "reference": "3170448f5769fe19f456173d833734e0ff1b84df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", + "reference": "3170448f5769fe19f456173d833734e0ff1b84df", "shasum": "" }, "require": { - "ext-filter": "^7.1", - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "doctrine/instantiator": "^1", - "mockery/mockery": "^1" + "mockery/mockery": "~1.3.2" }, "type": "library", "extra": { @@ -1155,7 +1181,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-02-22T12:28:44+00:00" + "time": "2020-07-20T20:05:34+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2398,12 +2424,13 @@ } ], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": { "james-heinrich/getid3": 20, - "myth/auth": 10 + "myth/auth": 20, + "codeigniter4/codeigniter4": 20 }, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=7.2" diff --git a/docs/setup-development.md b/docs/setup-development.md index 7991789a..47668c9a 100644 --- a/docs/setup-development.md +++ b/docs/setup-development.md @@ -97,21 +97,34 @@ docker ps -a ## Initialize and populate database -Build the database with the migrate command: +1. Build the database with the migrate command: ```bash # loads the database schema during first migration docker-compose run --rm app php spark migrate -all ``` -Populate the database with the required data: +2. Populate the database with the required data: ```bash # Populates all categories docker-compose run --rm app php spark db:seed CategorySeeder docker-compose run --rm app php spark db:seed LanguageSeeder +docker-compose run --rm app php spark db:seed PlatformSeeder +docker-compose run --rm app php spark db:seed AuthSeeder ``` +3. (optionnal) Populate the database with test data: + +```bash +docker-compose run --rm app php spark db:seed TestSeeder +``` + +This will add an active superadmin user with the following credentials: + +- username: **admin** +- password: **AGUehL3P** + ## Install/Update app dependencies Castopod uses `composer` to manage php dependencies and `npm` to manage javascript dependencies. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 96947df5..80664cbb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ -