refactor(auth): change contributor's role logic to have it included in the users_podcasts table

- update myth-auth and codeigniter to latest develop changes
- improve permission check: remove all
dynamic permissions per podcast and overwrite myth-auth services and permission filter
- remove
unnecessary code because of myth-auth upgrade
- refactor some controller code for better clarity
-
add remaining seeders in docs

closes #19, #20
This commit is contained in:
Yassine Doghri 2020-07-31 16:05:10 +00:00
parent e0da11517d
commit 58364bfed1
39 changed files with 1196 additions and 684 deletions

View File

@ -0,0 +1,76 @@
<?php namespace App\Authorization;
class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
{
//--------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------
/**
* Checks a group to see if they have the specified permission.
*
* @param int|string $permission
* @param int $groupId
*
* @return mixed
*/
public function groupHasPermission($permission, int $groupId)
{
if (
empty($permission) ||
(!is_string($permission) && !is_numeric($permission))
) {
return null;
}
if (empty($groupId) || !is_numeric($groupId)) {
return null;
}
// Get the Permission ID
$permissionId = $this->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;
}
}

View File

@ -0,0 +1,18 @@
<?php namespace App\Authorization;
class GroupModel extends \Myth\Auth\Authorization\GroupModel
{
public function getContributorRoles()
{
return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after')
->findAll();
}
public function getUserRoles()
{
return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after')
->findAll();
}
}

View File

@ -0,0 +1,63 @@
<?php namespace App\Authorization;
class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
{
/**
* Checks to see if a user, or one of their groups,
* has a specific permission.
*
* @param $userId
* @param $permissionId
*
* @return bool
*/
public function doesGroupHavePermission(
int $groupId,
int $permissionId
): bool {
// Check group permissions and take advantage of caching
$groupPerms = $this->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;
}
}

View File

@ -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

View File

@ -21,7 +21,7 @@ class Paths
* as this file.
*/
public $systemDirectory =
__DIR__ . '/../../vendor/codeigniter4/framework/system';
__DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
/*
*---------------------------------------------------------------

View File

@ -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');
}
);
/**
* --------------------------------------------------------------------

View File

@ -1,6 +1,12 @@
<?php namespace Config;
use CodeIgniter\Config\Services as CoreServices;
use CodeIgniter\Model;
use App\Authorization\FlatAuthorization;
use App\Authorization\PermissionModel;
use App\Authorization\GroupModel;
use App\Models\UserModel;
use Myth\Auth\Models\LoginModel;
require_once SYSTEMPATH . 'Config/Services.php';
@ -19,11 +25,68 @@ require_once SYSTEMPATH . 'Config/Services.php';
*/
class Services extends CoreServices
{
// public static function example($getShared = true)
// {
// if ($getShared) {
// return static::getSharedInstance('example');
// }
// return new \CodeIgniter\Example();
// }
public static function authentication(
string $lib = 'local',
Model $userModel = null,
Model $loginModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance(
'authentication',
$lib,
$userModel,
$loginModel
);
}
// config() checks first in app/Config
$config = config('Auth');
$class = $config->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);
}
}

View File

@ -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,
])
);
}
}

View File

@ -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();

View File

@ -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
{

View File

@ -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();

View File

@ -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,
])
);
}
}

View File

@ -1,98 +0,0 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use Myth\Auth\Models\UserModel;
class Auth extends \Myth\Auth\Controllers\AuthController
{
/**
* An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var array
*/
protected $helpers = ['auth'];
/**
* Displays the login form, or redirects
* the user to their destination/home if
* they are already logged in.
*/
public function changePassword()
{
return view('auth/change_password', [
'config' => $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'));
}
}

View File

@ -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');
}

View File

@ -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]);
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Class TestSeeder
* Inserts a superadmin user in the database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class TestSeeder extends Seeder
{
public function run()
{
/** Inserts an active user with the following credentials:
* username: admin
* password: AGUehL3P
*/
$this->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]);
}
}

View File

@ -9,7 +9,6 @@ namespace App\Entities;
use App\Models\PodcastModel;
use CodeIgniter\Entity;
use League\CommonMark\CommonMarkConverter;
use Parsedown;
class Episode extends Entity

View File

@ -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()

42
app/Entities/User.php Normal file
View File

@ -0,0 +1,42 @@
<?php namespace App\Entities;
use App\Models\PodcastModel;
class User extends \Myth\Auth\Entities\User
{
/**
* Per-user podcasts
* @var \App\Entities\Podcast[]
*/
protected $podcasts = [];
/**
* Array of field names and the type of value to cast them as
* when they are accessed.
*/
protected $casts = [
'active' => '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;
}
}

115
app/Filters/Permission.php Normal file
View File

@ -0,0 +1,115 @@
<?php namespace App\Filters;
use App\Models\PodcastModel;
use Config\Services;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;
use Myth\Auth\Exceptions\PermissionException;
class Permission implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* @param \CodeIgniter\HTTP\RequestInterface $request
* @param array|null $params
*
* @return mixed
*/
public function before(RequestInterface $request, $params = null)
{
if (!function_exists('logged_in')) {
helper('auth');
}
if (empty($params)) {
return;
}
$authenticate = Services::authentication();
// if no user is logged in then send to the login form
if (!$authenticate->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
) {
}
//--------------------------------------------------------------------
}

View File

@ -1,19 +0,0 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
/**
* Gets the permission name by concatenating the context and action
*
* @param string $context
* @param string $action
*
* @return string permission name
*/
function get_permission($context, $action)
{
return $context . '-' . $action;
}

View File

@ -21,3 +21,16 @@ function get_browser_language($http_accept_language)
return null;
}
/**
* Check if a string starts with some characters
*
* @param string $string
* @param string $query
*
* @return bool
*/
function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
}

View File

@ -10,6 +10,7 @@
'podcasts' => 'Podcasts',
'users' => 'Users',
'admin_home' => 'Home',
'my_podcasts' => 'My podcasts',
'podcast_list' => 'All podcasts',
'podcast_create' => 'New podcast',
'user_list' => 'All users',

View File

@ -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}',

View File

@ -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',
]

View File

@ -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;
}
}

28
app/Models/UserModel.php Normal file
View File

@ -0,0 +1,28 @@
<?php namespace App\Models;
use App\Entities\User;
class UserModel extends \Myth\Auth\Models\UserModel
{
protected $returnType = User::class;
public function getPodcastContributors($podcast_id)
{
return $this->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();
}
}

View File

@ -16,7 +16,7 @@
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
'Podcast.edit'
) ?>"><?= icon('edit') ?></a>
<a class="inline-flex p-2 bg-gray-100 rounded-full shadow-xs text-teal-gray hover:bg-gray-200" href="<?= route_to(
<a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
'podcast_view',
$podcast->id
) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(

View File

@ -3,7 +3,7 @@ $navigation = [
'dashboard' => ['icon' => 'dashboard', 'items' => ['admin_home']],
'podcasts' => [
'icon' => 'mic',
'items' => ['podcast_list', 'podcast_create'],
'items' => ['my_podcasts', 'podcast_list', 'podcast_create'],
],
'users' => ['icon' => 'group', 'items' => ['user_list', 'user_create']],
]; ?>

View File

@ -19,7 +19,7 @@
<thead>
<tr>
<th class="px-4 py-2">Username</th>
<th class="px-4 py-2">Permissions</th>
<th class="px-4 py-2">Role</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
@ -27,10 +27,7 @@
<?php foreach ($podcast->contributors as $contributor): ?>
<tr>
<td class="px-4 py-2 border"><?= $contributor->username ?></td>
<td class="px-4 py-2 border">[<?= implode(
', ',
$contributor->permissions
) ?>]</td>
<td class="px-4 py-2 border"><?= $contributor->podcast_role ?></td>
<td class="px-4 py-2 border">
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
'contributor_edit',

View File

@ -37,8 +37,6 @@
<textarea class="hidden form-textarea" id="description" name="description" required data-editor="markdown"><?= old(
'description'
) ?></textarea>
<button type="button" data-editor-view="markdown">Markdown</button>
<button type="button" data-editor-view="wysiwyg">WYSIWYG</button>
</div>
<div class="flex flex-col mb-4">

View File

@ -162,7 +162,9 @@
<label for="custom_html_head"><?= esc(
lang('Podcast.form.custom_html_head')
) ?></label>
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"></textarea>
<textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"><?= old(
'custom_html_head'
) ?></textarea>
</div>
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(

View File

@ -0,0 +1,33 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('User.edit_roles', ['username' => $user->username]) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<form action="<?= route_to(
'user_edit',
$user->id
) ?>" method="post" class="flex flex-col max-w-lg">
<?= csrf_field() ?>
<label for="roles"><?= lang('User.form.roles') ?></label>
<select id="roles" name="roles[]" autocomplete="off" class="mb-6 form-multiselect" multiple>
<?php foreach ($roles as $role): ?>
<option value="<?= $role->id ?>"
<?php if (
in_array($role->name, $user->roles)
): ?> selected <?php endif; ?>>
<?= $role->name ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="px-4 py-2 ml-auto border">
<?= lang('User.form.submit_edit') ?>
</button>
</form>
<?= $this->endSection() ?>

View File

@ -1,3 +1,5 @@
<?php helper('html'); ?>
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
@ -12,7 +14,7 @@
<tr>
<th class="px-4 py-2">Username</th>
<th class="px-4 py-2">Email</th>
<th class="px-4 py-2">Permissions</th>
<th class="px-4 py-2">Roles</th>
<th class="px-4 py-2">Banned?</th>
<th class="px-4 py-2">Actions</th>
</tr>
@ -22,15 +24,23 @@
<tr>
<td class="px-4 py-2 border"><?= $user->username ?></td>
<td class="px-4 py-2 border"><?= $user->email ?></td>
<td class="px-4 py-2 border">[<?= implode(
', ',
$user->permissions
) ?>]</td>
<td class="px-4 py-2 border">
[<?= implode(', ', $user->roles) ?>]
<a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
'user_edit',
$user->id
) ?>" data-toggle="tooltip" data-placement="bottom"
title="<?= lang('User.edit_roles', [
'username' => $user->username,
]) ?>">
<?= icon('edit') ?>
</a>
</td>
<td class="px-4 py-2 border"><?= $user->isBanned()
? 'Yes'
: 'No' ?></td>
<td class="px-4 py-2 border">
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
'user_force_pass_reset',
$user->id
) ?>"><?= lang('User.forcePassReset') ?></a>

View File

@ -1,29 +0,0 @@
<?= $this->extend($config->viewLayout) ?>
<?= $this->section('title') ?>
<?= lang('Auth.resetYourPassword') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<form action="<?= route_to(
'change-password'
) ?>" method="post" class="flex flex-col">
<?= csrf_field() ?>
<input type="hidden" name="token" value="<?= $token ?>">
<input type="hidden" name="email" value="<?= $email ?>">
<label for="password"><?= lang('Auth.newPassword') ?></label>
<input type="password" class="mb-4 form-input" name="password">
<label for="pass_confirm"><?= lang('Auth.newPasswordRepeat') ?></label>
<input type="password" class="mb-6 form-input" name="pass_confirm">
<button type="submit" class="px-4 py-2 ml-auto border">
<?= lang('Auth.resetPassword') ?>
</button>
</form>
<?= $this->endSection() ?>

View File

@ -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"
}
]
}

97
composer.lock generated
View File

@ -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"

View File

@ -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.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
<phpunit bootstrap="vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"