Commit d58e5187 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: add user permissions and basic groups to handle authorizations

- add AuthSeeder to bootstrap authorization data and remove UserSeeder
- create a superadmin group having all authorizations
- refactor routes and controller methods to separate get and post requests
- refactor admin views with a title section in layout
- add contributors section to podcasts to manage contributions (add, edit roles and remove)

closes #3, #18
parent c63a0776
......@@ -36,11 +36,12 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}');
// route since we don't have to scan directories.
$routes->get('/', 'Home::index', ['as' => 'home']);
// Public routes
$routes->group('@(:podcastName)', function ($routes) {
$routes->add('/', 'Podcast/$1', ['as' => 'podcast']);
$routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
$routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
$routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [
$routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
$routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [
'as' => 'episode',
]);
});
......@@ -51,74 +52,119 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
]);
// Show the Unknown UserAgents
$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents');
$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
$routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
// Admin area
$routes->group(
config('App')->adminGateway,
['namespace' => 'App\Controllers\Admin'],
function ($routes) {
$routes->add('/', 'Home', [
$routes->get('/', 'Home', [
'as' => 'admin',
]);
$routes->add('new-podcast', 'Podcast::create', [
$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',
'filter' => 'permission:podcasts-create',
]);
$routes->post('new-podcast', 'Podcast::attemptCreate', [
'filter' => 'permission:podcasts-create',
]);
$routes->add('podcasts', 'Podcast::list', ['as' => 'podcast_list']);
$routes->group('podcasts/@(:podcastName)', function ($routes) {
$routes->add('edit', 'Podcast::edit/$1', [
// Use ids in admin area to help permission and group lookups
$routes->group('podcasts/(:num)', function ($routes) {
$routes->get('edit', 'Podcast::edit/$1', [
'as' => 'podcast_edit',
]);
$routes->post('edit', 'Podcast::attemptEdit/$1');
$routes->add('delete', 'Podcast::delete/$1', [
'as' => 'podcast_delete',
]);
$routes->add('new-episode', 'Episode::create/$1', [
// Podcast episodes
$routes->get('episodes', 'Episode::list/$1', [
'as' => 'episode_list',
]);
$routes->get('new-episode', 'Episode::create/$1', [
'as' => 'episode_create',
]);
$routes->add('episodes', 'Episode::list/$1', [
'as' => 'episode_list',
$routes->post('new-episode', 'Episode::attemptCreate/$1');
$routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
'as' => 'episode_edit',
]);
$routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2');
$routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [
'as' => 'episode_delete',
]);
$routes->add(
'episodes/(:episodeSlug)/edit',
'Episode::edit/$1/$2',
// Podcast contributors
$routes->get('contributors', 'Contributor::list/$1', [
'as' => 'contributor_list',
]);
$routes->get('add-contributor', 'Contributor::add/$1', [
'as' => 'contributor_add',
]);
$routes->post('add-contributor', 'Contributor::attemptAdd/$1');
$routes->get(
'contributors/(:num)/edit',
'Contributor::edit/$1/$2',
[
'as' => 'episode_edit',
'as' => 'contributor_edit',
]
);
$routes->post(
'contributors/(:num)/edit',
'Contributor::attemptEdit/$1/$2'
);
$routes->add(
'episodes/(:episodeSlug)/delete',
'Episode::delete/$1/$2',
[
'as' => 'episode_delete',
]
'contributors/(:num)/remove',
'Contributor::remove/$1/$2',
['as' => 'contributor_remove']
);
});
// Users
$routes->add('users', 'User::list', ['as' => 'user_list']);
$routes->add('new-user', 'User::create', ['as' => 'user_create']);
$routes->get('users', 'User::list', [
'as' => 'user_list',
'filter' => 'permission:users-list',
]);
$routes->get('new-user', 'User::create', [
'as' => 'user_create',
'filter' => 'permission:users-create',
]);
$routes->post('new-user', 'User::attemptCreate', [
'filter' => 'permission:users-create',
]);
$routes->add('users/@(:any)/ban', 'User::ban/$1', [
$routes->add('users/(:num)/ban', 'User::ban/$1', [
'as' => 'user_ban',
'filter' => 'permission:users-manage_bans',
]);
$routes->add('users/@(:any)/unban', 'User::unBan/$1', [
$routes->add('users/(:num)/unban', 'User::unBan/$1', [
'as' => 'user_unban',
'filter' => 'permission:users-manage_bans',
]);
$routes->add(
'users/@(:any)/force-pass-reset',
'users/(:num)/force-pass-reset',
'User::forcePassReset/$1',
[
'as' => 'user_force_pass_reset',
'filter' => 'permission:users-force_pass_reset',
]
);
$routes->add('users/@(:any)/delete', 'User::delete/$1', [
$routes->add('users/(:num)/delete', 'User::delete/$1', [
'as' => 'user_delete',
'filter' => 'permission:users-delete',
]);
// My account
......
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PodcastModel;
use Myth\Auth\Authorization\GroupModel;
use Myth\Auth\Config\Services;
use Myth\Auth\Models\UserModel;
class Contributor extends BaseController
{
protected \App\Entities\Podcast $podcast;
protected ?\Myth\Auth\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]);
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())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function list()
{
$data = [
'podcast' => $this->podcast,
];
echo view('admin/contributor/list', $data);
}
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,
];
echo view('admin/contributor/add', $data);
}
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
);
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,
];
echo view('admin/contributor/edit', $data);
}
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')
);
return redirect()->route('contributor_list', [$this->podcast->id]);
}
public function remove()
{
$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'
)
->like('name', 'podcasts:' . $this->podcast->id, 'after')
->where('user_id', $this->user->id)
->first();
$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]);
}
}
......@@ -17,23 +17,52 @@ 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->where('name', $params[0])->first();
$this->podcast = $podcast_model->find($params[0]);
if (count($params) > 1) {
$episode_model = new EpisodeModel();
if (
!($episode = $episode_model
!($this->episode = $episode_model
->where([
'podcast_id' => $this->podcast->id,
'slug' => $params[1],
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
}
return $this->$method();
......@@ -41,13 +70,8 @@ class Episode extends BaseController
public function list()
{
$episode_model = new EpisodeModel();
$data = [
'podcast' => $this->podcast,
'all_podcast_episodes' => $episode_model
->where('podcast_id', $this->podcast->id)
->find(),
];
return view('admin/episode/list', $data);
......@@ -57,105 +81,118 @@ class Episode extends BaseController
{
helper(['form']);
if (
!$this->validate([
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'title' => 'required',
'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
'description' => 'required',
'type' => 'required',
])
) {
$data = [
'podcast' => $this->podcast,
];
echo view('admin/episode/create', $data);
} else {
$new_episode = new \App\Entities\Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getVar('title'),
'slug' => $this->request->getVar('slug'),
'enclosure' => $this->request->getFile('enclosure'),
'pub_date' => $this->request->getVar('pub_date'),
'description' => $this->request->getVar('description'),
'image' => $this->request->getFile('image'),
'explicit' => $this->request->getVar('explicit') or false,
'number' => $this->request->getVar('episode_number'),
'season_number' => $this->request->getVar('season_number'),
'type' => $this->request->getVar('type'),
'author_name' => $this->request->getVar('author_name'),
'author_email' => $this->request->getVar('author_email'),
'block' => $this->request->getVar('block') or false,
]);
$data = [
'podcast' => $this->podcast,
];
$episode_model = new EpisodeModel();
$episode_model->save($new_episode);
echo view('admin/episode/create', $data);
}
return redirect()->route('episode_list', [$this->podcast->name]);
public function attemptCreate()
{
$rules = [
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$new_episode = new \App\Entities\Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'enclosure' => $this->request->getFile('enclosure'),
'pub_date' => $this->request->getPost('pub_date'),
'description' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'),
'explicit' => (bool) $this->request->getPost('explicit'),
'number' => $this->request->getPost('episode_number'),
'season_number' => $this->request->getPost('season_number'),
'type' => $this->request->getPost('type'),
'author_name' => $this->request->getPost('author_name'),
'author_email' => $this->request->getPost('author_email'),
'block' => (bool) $this->request->getPost('block'),
]);
$episode_model = new EpisodeModel();
if (!$episode_model->save($new_episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episode_model->errors());
}
return redirect()->route('episode_list', [$this->podcast->id]);
}
public function edit()
{
helper(['form']);
if (
!$this->validate([
'enclosure' =>
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'title' => 'required',
'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
'description' => 'required',
'type' => 'required',
])
) {
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
echo view('admin/episode/edit', $data);
} else {
$this->episode->title = $this->request->getVar('title');
$this->episode->slug = $this->request->getVar('slug');
$this->episode->pub_date = $this->request->getVar('pub_date');
$this->episode->description = $this->request->getVar('description');
$this->episode->explicit =
($this->request->getVar('explicit') or false);
$this->episode->number = $this->request->getVar('episode_number');
$this->episode->season_number = $this->request->getVar(
'season_number'
)
? $this->request->getVar('season_number')
: null;
$this->episode->type = $this->request->getVar('type');
$this->episode->author_name = $this->request->getVar('author_name');
$this->episode->author_email = $this->request->getVar(
'author_email'
);
$this->episode->block = ($this->request->getVar('block') or false);
$enclosure = $this->request->getFile('enclosure');
if ($enclosure->isValid()) {
$this->episode->enclosure = $this->request->getFile(
'enclosure'
);
}
$image = $this->request->getFile('image');
if ($image) {
$this->episode->image = $this->request->getFile('image');
}
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$episode_model = new EpisodeModel();
$episode_model->save($this->episode);
echo view('admin/episode/edit', $data);
}
public function attemptEdit()
{
$rules = [
'enclosure' =>
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
return redirect()->route('episode_list', [$this->podcast->name]);
$this->episode->title = $this->request->getPost('title');
$this->episode->slug = $this->request->getPost('slug');
$this->episode->pub_date = $this->request->getPost('pub_date');
$this->episode->description = $this->request->getPost('description');
$this->episode->explicit = (bool) $this->request->getPost('explicit');
$this->episode->number = $this->request->getPost('episode_number');
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null;
$this->episode->type = $this->request->getPost('type');
$this->episode->author_name = $this->request->getPost('author_name');
$this->episode->author_email = $this->request->getPost('author_email');