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:
parent
e0da11517d
commit
58364bfed1
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -21,7 +21,7 @@ class Paths
|
|||
* as this file.
|
||||
*/
|
||||
public $systemDirectory =
|
||||
__DIR__ . '/../../vendor/codeigniter4/framework/system';
|
||||
__DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
|
||||
|
||||
/*
|
||||
*---------------------------------------------------------------
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ namespace App\Entities;
|
|||
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Entity;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use Parsedown;
|
||||
|
||||
class Episode extends Entity
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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']],
|
||||
]; ?>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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() ?>
|
|
@ -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>
|
||||
|
|
|
@ -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() ?>
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue