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
This commit is contained in:
Yassine Doghri 2020-07-16 10:08:23 +00:00
parent c63a077618
commit d58e51874a
42 changed files with 1431 additions and 583 deletions

View File

@ -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', [
'as' => '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', [
'as' => 'episode_create',
]);
$routes->add('episodes', 'Episode::list/$1', [
// Podcast episodes
$routes->get('episodes', 'Episode::list/$1', [
'as' => 'episode_list',
]);
$routes->get('new-episode', 'Episode::create/$1', [
'as' => 'episode_create',
]);
$routes->post('new-episode', 'Episode::attemptCreate/$1');
$routes->add(
'episodes/(:episodeSlug)/edit',
'Episode::edit/$1/$2',
$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',
]);
// 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->add('users/@(:any)/ban', 'User::ban/$1', [
'as' => 'user_ban',
$routes->get('users', 'User::list', [
'as' => 'user_list',
'filter' => 'permission:users-list',
]);
$routes->add('users/@(:any)/unban', 'User::unBan/$1', [
$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/(:num)/ban', 'User::ban/$1', [
'as' => 'user_ban',
'filter' => 'permission:users-manage_bans',
]);
$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

View File

@ -0,0 +1,187 @@
<?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]);
}
}

View File

@ -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,
];
$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,
]);
echo view('admin/episode/create', $data);
}
$episode_model = new EpisodeModel();
$episode_model->save($new_episode);
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',
];
return redirect()->route('episode_list', [$this->podcast->name]);
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,
];
$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);
echo view('admin/episode/edit', $data);
}
$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');
}
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',
];
$episode_model = new EpisodeModel();
$episode_model->save($this->episode);
return redirect()->route('episode_list', [$this->podcast->name]);
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$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');
$this->episode->block = (bool) $this->request->getPost('block');
$enclosure = $this->request->getFile('enclosure');
if ($enclosure->isValid()) {
$this->episode->enclosure = $enclosure;
}
$image = $this->request->getFile('image');
if ($image) {
$this->episode->image = $image;
}
$episode_model = new EpisodeModel();
if (!$episode_model->save($this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episode_model->errors());
}
return redirect()->route('episode_list', [$this->podcast->id]);
}
public function delete()
@ -163,6 +200,6 @@ class Episode extends BaseController
$episode_model = new EpisodeModel();
$episode_model->delete($this->episode->id);
return redirect()->route('episode_list', [$this->podcast->name]);
return redirect()->route('episode_list', [$this->podcast->id]);
}
}

View File

@ -6,7 +6,6 @@
*/
namespace App\Controllers\Admin;
use App\Entities\UserPodcast;
use App\Models\CategoryModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
@ -18,18 +17,59 @@ class Podcast extends BaseController
public function _remap($method, ...$params)
{
if (count($params) > 0) {
switch ($method) {
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')
);
}
case 'listContributors':
case 'addContributor':
case 'editContributor':
case 'deleteContributor':
if (
!has_permission('podcasts-manage_contributors') ||
!has_permission(
"podcasts:$params[0]-manage_contributors"
)
) {
throw new \RuntimeException(
lang('Auth.notEnoughPrivilege')
);
}
}
$podcast_model = new PodcastModel();
if (
!($podcast = $podcast_model->where('name', $params[0])->first())
) {
if (!($this->podcast = $podcast_model->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
}
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();
@ -42,133 +82,141 @@ class Podcast extends BaseController
public function create()
{
helper(['form', 'misc']);
$podcast_model = new PodcastModel();
if (
!$this->validate([
'title' => 'required',
'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
'description' => 'required|max_length[4000]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
'owner_email' => 'required|valid_email',
'type' => 'required',
])
) {
$languageModel = new LanguageModel();
$categoryModel = new CategoryModel();
$data = [
'languages' => $languageModel->findAll(),
'categories' => $categoryModel->findAll(),
'browser_lang' => get_browser_language(
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
),
];
$languageModel = new LanguageModel();
$categoryModel = new CategoryModel();
$data = [
'languages' => $languageModel->findAll(),
'categories' => $categoryModel->findAll(),
'browser_lang' => get_browser_language(
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
),
];
echo view('admin/podcast/create', $data);
} else {
$podcast = new \App\Entities\Podcast([
'title' => $this->request->getVar('title'),
'name' => $this->request->getVar('name'),
'description' => $this->request->getVar('description'),
'episode_description_footer' => $this->request->getVar(
'episode_description_footer'
),
'image' => $this->request->getFile('image'),
'language' => $this->request->getVar('language'),
'category' => $this->request->getVar('category'),
'explicit' => $this->request->getVar('explicit') or false,
'author_name' => $this->request->getVar('author_name'),
'author_email' => $this->request->getVar('author_email'),
'owner_name' => $this->request->getVar('owner_name'),
'owner_email' => $this->request->getVar('owner_email'),
'type' => $this->request->getVar('type'),
'copyright' => $this->request->getVar('copyright'),
'block' => $this->request->getVar('block') or false,
'complete' => $this->request->getVar('complete') or false,
'custom_html_head' => $this->request->getVar(
'custom_html_head'
),
]);
echo view('admin/podcast/create', $data);
}
$db = \Config\Database::connect();
public function attemptCreate()
{
$rules = [
'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
];
$db->transStart();
$new_podcast_id = $podcast_model->insert($podcast, true);
$user_podcast_model = new \App\Models\UserPodcastModel();
$user_podcast_model->save([
'user_id' => user()->id,
'podcast_id' => $new_podcast_id,
]);
$db->transComplete();
return redirect()->route('podcast_list', [$podcast->name]);
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$podcast = new \App\Entities\Podcast([
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description' => $this->request->getPost('description'),
'episode_description_footer' => $this->request->getPost(
'episode_description_footer'
),
'image' => $this->request->getFile('image'),
'language' => $this->request->getPost('language'),
'category' => $this->request->getPost('category'),
'explicit' => (bool) $this->request->getPost('explicit'),
'author_name' => $this->request->getPost('author_name'),
'author_email' => $this->request->getPost('author_email'),
'owner' => user(),
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'block' => (bool) $this->request->getPost('block'),
'complete' => (bool) $this->request->getPost('complete'),
'custom_html_head' => $this->request->getPost('custom_html_head'),
]);
$podcast_model = new PodcastModel();
$db = \Config\Database::connect();
$db->transStart();
if (!($new_podcast_id = $podcast_model->insert($podcast, true))) {
$db->transComplete();
return redirect()
->back()
->withInput()
->with('errors', $podcast_model->errors());
}
$podcast_model->addContributorToPodcast(user()->id, $new_podcast_id);
$db->transComplete();
return redirect()->route('podcast_list');
}
public function edit()
{
helper(['form', 'misc']);
helper('form');
if (
!$this->validate([
'title' => 'required',
'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
'description' => 'required|max_length[4000]',
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
'owner_email' => 'required|valid_email',
'type' => 'required',
])
) {
$languageModel = new LanguageModel();
$categoryModel = new CategoryModel();
$data = [
'podcast' => $this->podcast,
'languages' => $languageModel->findAll(),
'categories' => $categoryModel->findAll(),
];
$languageModel = new LanguageModel();
$categoryModel = new CategoryModel();
$data = [
'podcast' => $this->podcast,
'languages' => $languageModel->findAll(),
'categories' => $categoryModel->findAll(),
];
echo view('admin/podcast/edit', $data);
} else {
$this->podcast->title = $this->request->getVar('title');
$this->podcast->name = $this->request->getVar('name');
$this->podcast->description = $this->request->getVar('description');
$this->podcast->episode_description_footer = $this->request->getVar(
'episode_description_footer'
);
echo view('admin/podcast/edit', $data);
}
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->podcast->image = $this->request->getFile('image');
}
$this->podcast->language = $this->request->getVar('language');
$this->podcast->category = $this->request->getVar('category');
$this->podcast->explicit =
($this->request->getVar('explicit') or false);
$this->podcast->author_name = $this->request->getVar('author_name');
$this->podcast->author_email = $this->request->getVar(
'author_email'
);
$this->podcast->owner_name = $this->request->getVar('owner_name');
$this->podcast->owner_email = $this->request->getVar('owner_email');
$this->podcast->type = $this->request->getVar('type');
$this->podcast->copyright = $this->request->getVar('copyright');
$this->podcast->block = ($this->request->getVar('block') or false);
$this->podcast->complete =
($this->request->getVar('complete') or false);
$this->podcast->custom_html_head = $this->request->getVar(
'custom_html_head'
);
public function attemptEdit()
{
$rules = [
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
];
$podcast_model = new PodcastModel();
$podcast_model->save($this->podcast);
return redirect()->route('podcast_list', [$this->podcast->name]);
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->podcast->title = $this->request->getPost('title');
$this->podcast->name = $this->request->getPost('name');
$this->podcast->description = $this->request->getPost('description');
$this->podcast->episode_description_footer = $this->request->getPost(
'episode_description_footer'
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->podcast->image = $image;
}
$this->podcast->language = $this->request->getPost('language');
$this->podcast->category = $this->request->getPost('category');
$this->podcast->explicit = (bool) $this->request->getPost('explicit');
$this->podcast->author_name = $this->request->getPost('author_name');
$this->podcast->author_email = $this->request->getPost('author_email');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->block = (bool) $this->request->getPost('block');
$this->podcast->complete = (bool) $this->request->getPost('complete');
$this->podcast->custom_html_head = $this->request->getPost(
'custom_html_head'
);
$podcast_model = new PodcastModel();
if (!$podcast_model->save($this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcast_model->errors());
}
return redirect()->route('podcast_list');
}
public function delete()

View File

@ -17,12 +17,9 @@ class User extends BaseController
{
if (count($params) > 0) {
$user_model = new UserModel();
if (
!($user = $user_model->where('username', $params[0])->first())
) {
if (!($this->user = $user_model->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->user = $user;
}
return $this->$method();
@ -38,6 +35,11 @@ class User extends BaseController
}
public function create()
{
echo view('admin/user/create');
}
public function attemptCreate()
{
$user_model = new UserModel();
@ -53,30 +55,33 @@ class User extends BaseController
);
if (!$this->validate($rules)) {
echo view('admin/user/create');
} else {
// Save the user
$user = new \Myth\Auth\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();
if (!$user_model->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $user_model->errors());
}
// Success!
return redirect()
->route('user_list')
->with('message', lang('User.createSuccess'));
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new \Myth\Auth\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();
if (!$user_model->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $user_model->errors());
}
// Success!
return redirect()
->route('user_list')
->with('message', lang('User.createSuccess'));
}
public function forcePassReset()

View File

@ -24,7 +24,7 @@ class Episode extends BaseController
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],
@ -33,7 +33,6 @@ class Episode extends BaseController
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
}
return $this->$method();

View File

@ -17,11 +17,12 @@ class Podcast extends BaseController
if (count($params) > 0) {
$podcast_model = new PodcastModel();
if (
!($podcast = $podcast_model->where('name', $params[0])->first())
!($this->podcast = $podcast_model
->where('name', $params[0])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
}
return $this->$method();

View File

@ -81,17 +81,23 @@ class AddPodcasts extends Migration
'Email of the group responsible for creating the show. Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists. Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.',
'null' => true,
],
'owner_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'comment' => 'The podcast owner.',
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'owner_name' =>
'comment' =>
'The podcast owner name. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts.',
'null' => true,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 1024,
'owner_email' =>
'comment' =>
'The podcast owner email address. Note: The owner information is for administrative communication about the podcast and isnt displayed in Apple Podcasts. Please make sure the email address is active and monitored.',
'null' => true,
],
@ -147,6 +153,7 @@ class AddPodcasts extends Migration
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('owner_id', 'users', 'id');
$this->forge->createTable('podcasts');
}

View File

@ -17,12 +17,6 @@ class AddUsersPodcasts extends Migration
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'auto_increment' => true,
],
'user_id' => [
'type' => 'INT',
'constraint' => 11,
@ -34,8 +28,7 @@ class AddUsersPodcasts extends Migration
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['user_id', 'podcast_id']);
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->createTable('users_podcasts');

View File

@ -0,0 +1,153 @@
<?php
/**
* Class PermissionSeeder
* Inserts permissions
*
* @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 AuthSeeder extends Seeder
{
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' => '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',
],
],
];
// Map permissions to a format the `auth_permissions` table expects
$data_permissions = [];
$data_groups_permissions = [];
$permission_id = 0;
foreach ($permissions as $context => $actions) {
foreach ($actions as $action) {
array_push($data_permissions, [
'id' => ++$permission_id,
'name' => get_permission($context, $action['name']),
'description' => $action['description'],
]);
// add all permissions to superadmin
array_push($data_groups_permissions, [
'group_id' => 1,
'permission_id' => $permission_id,
]);
}
}
$this->db->table('auth_permissions')->insertBatch($data_permissions);
$this->db->table('auth_groups')->insertBatch($groups);
$this->db
->table('auth_groups_permissions')
->insertBatch($data_groups_permissions);
// TODO: Remove superadmin user as it is used for testing purposes
$this->db->table('users')->insert([
'id' => 1,
'username' => 'admin',
'email' => 'admin@castopod.com',
'password_hash' =>
// password: AGUehL3P
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
]);
$this->db
->table('auth_groups_users')
->insert(['group_id' => 1, 'user_id' => 1]);
}
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Class UserSeeder
* Inserts 'admin' user in users table for testing purposes
*
* @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 UserSeeder extends Seeder
{
public function run()
{
$data = [
'username' => 'admin',
'email' => 'admin@castopod.com',
'password_hash' =>
// password: AGUehL3P
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
];
$this->db->table('users')->insert($data);
}
}

View File

@ -9,6 +9,7 @@ namespace App\Entities;
use App\Models\EpisodeModel;
use CodeIgniter\Entity;
use Myth\Auth\Models\UserModel;
class Podcast extends Entity
{
@ -17,6 +18,8 @@ class Podcast extends Entity
protected $image_media_path;
protected $image_url;
protected $episodes;
protected $owner;
protected $contributors;
protected $casts = [
'id' => 'integer',
@ -29,6 +32,7 @@ class Podcast extends Entity
'explicit' => 'boolean',
'author_name' => '?string',
'author_email' => '?string',
'owner_id' => 'integer',
'owner_name' => '?string',
'owner_email' => '?string',
'type' => 'string',
@ -92,7 +96,7 @@ class Podcast extends Entity
);
}
if (empty($this->permissions)) {
if (empty($this->episodes)) {
$this->episodes = (new EpisodeModel())->getPodcastEpisodes(
$this->id
);
@ -100,4 +104,40 @@ class Podcast extends Entity
return $this->episodes;
}
/**
* Returns the podcast owner
*
* @return \Myth\Auth\Entities\User
*/
public function getOwner()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting owner.'
);
}
if (empty($this->owner)) {
$this->owner = (new UserModel())->find($this->owner_id);
}
return $this->owner;
}
public function setOwner(\Myth\Auth\Entities\User $user)
{
$this->attributes['owner_id'] = $user->id;
return $this;
}
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();
}
}

View File

@ -1,18 +0,0 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
class UserPodcast extends Entity
{
protected $casts = [
'user_id' => 'integer',
'podcast_id' => 'integer',
];
}

View File

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

View File

@ -0,0 +1,21 @@
<?
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'podcast_contributors' => 'Podcast contributors',
'add' => 'Add contributor',
'add_contributor' => 'Add a contributor for {0}',
'edit_role' => 'Update role for {0}',
'edit' => 'Edit',
'remove' => 'Remove',
'form' => [
'user' => 'User',
'role' => 'Role',
'submit_add' => 'Add contributor',
'submit_edit' => 'Update role'
]
];

View File

@ -7,7 +7,6 @@
return [
'all_podcast_episodes' => 'All podcast episodes',
'create_one' => 'Add a new one',
'back_to_podcast' => 'Go back to podcast',
'edit' => 'Edit',
'delete' => 'Delete',

View File

@ -7,5 +7,6 @@
return [
'passwordChangeSuccess' => 'Password has been successfully changed!',
'changePassword' => 'Change my password'
'changePassword' => 'Change my password',
'info' => 'My account info'
];

View File

@ -8,13 +8,13 @@
return [
'all_podcasts' => 'All podcasts',
'no_podcast' => 'No podcast found!',
'create_one' => 'Add a new one',
'create' => 'Create a Podcast',
'new_episode' => 'New Episode',
'feed' => 'RSS feed',
'edit' => 'Edit',
'delete' => 'Delete',
'see_episodes' => 'See episodes',
'see_contributors' => 'See contributors',
'goto_page' => 'Go to page',
'form' => [
'title' => 'Title',

View File

@ -10,11 +10,13 @@ return [
'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.',
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'Create a user',
'all_users' => 'All users',
'form' => [
'email' => 'Email',
'username' => 'Username',

View File

@ -19,11 +19,8 @@ class EpisodeModel extends Model
'title',
'slug',
'enclosure_uri',
'enclosure_length',
'enclosure_type',
'pub_date',
'description',
'duration',
'image_uri',
'explicit',
'number',
@ -39,6 +36,21 @@ class EpisodeModel extends Model
protected $useSoftDeletes = true;
protected $useTimestamps = true;
protected $validationRules = [
'podcast_id' => 'required',
'title' => 'required',
'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]',
'enclosure_uri' => 'required',
'pub_date' => 'required|valid_date',
'description' => 'required',
'image_uri' => 'required',
'number' => 'required',
'season_number' => 'required',
'author_email' => 'valid_email|permit_empty',
'type' => 'required',
];
protected $validationMessages = [];
protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache'];

View File

@ -8,6 +8,8 @@
namespace App\Models;
use CodeIgniter\Model;
use Myth\Auth\Authorization\GroupModel;
use Myth\Auth\Config\Services;
class PodcastModel extends Model
{
@ -26,6 +28,7 @@ class PodcastModel extends Model
'explicit',
'author_name',
'author_email',
'owner_id',
'owner_name',
'owner_email',
'type',
@ -40,8 +43,60 @@ class PodcastModel extends Model
protected $useTimestamps = true;
protected $afterInsert = ['clearCache'];
protected $validationRules = [
'title' => 'required',
'name' =>
'required|regex_match[/^[a-zA-Z0-9\_]{1,191}$/]|is_unique[podcasts.name,id,{id}]',
'description' => 'required',
'image_uri' => 'required',
'language' => 'required',
'category' => 'required',
'author_email' => 'valid_email|permit_empty',
'owner_id' => 'required',
'owner_email' => 'required|valid_email',
'type' => 'required',
];
protected $validationMessages = [];
protected $afterInsert = ['clearCache', 'createPodcastPermissions'];
protected $afterUpdate = ['clearCache'];
protected $beforeDelete = ['clearCache'];
/**
* Gets all the podcasts a given user is contributing to
*
* @param int $user_id
*
* @return \App\Entities\Podcast[] podcasts
*/
public function getUserPodcasts($user_id)
{
return $this->select('podcasts.*')
->join('users_podcasts', 'users_podcasts.podcast_id = podcasts.id')
->where('users_podcasts.user_id', $user_id)
->findAll();
}
public function addContributorToPodcast($user_id, $podcast_id)
{
$data = [
'user_id' => (int) $user_id,
'podcast_id' => (int) $podcast_id,
];
return $this->db->table('users_podcasts')->insert($data);
}
public function removeContributorFromPodcast($user_id, $podcast_id)
{
return $this->db
->table('users_podcasts')
->where([
'user_id' => $user_id,
'podcast_id' => $podcast_id,
])
->delete();
}
protected function clearCache(array $data)
{
@ -56,5 +111,95 @@ class PodcastModel extends Model
// foreach ($podcast->episodes as $episode) {
// $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' => '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;
}
}

View File

@ -1,23 +0,0 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use CodeIgniter\Model;
class UserPodcastModel extends Model
{
protected $table = 'users_podcasts';
protected $primaryKey = 'id';
protected $allowedFields = ['user_id', 'podcast_id'];
protected $returnType = 'App\Entities\UserPodcast';
protected $useSoftDeletes = false;
protected $useTimestamps = false;
}

View File

@ -1,17 +1,17 @@
<?php if (session()->has('message')): ?>
<div class="px-4 py-2 font-semibold text-green-900 bg-green-200 border border-green-700">
<div class="px-4 py-2 mb-4 font-semibold text-green-900 bg-green-200 border border-green-700">
<?= session('message') ?>
</div>
<?php endif; ?>
<?php if (session()->has('error')): ?>
<div class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700">
<div class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700">
<?= session('error') ?>
</div>
<?php endif; ?>
<?php if (session()->has('errors')): ?>
<ul class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700">
<ul class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700">
<?php foreach (session('errors') as $error): ?>
<li><?= $error ?></li>
<?php endforeach; ?>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Castopod</title>
<title>Castopod Admin</title>
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
@ -12,9 +12,12 @@
<body class="flex flex-col min-h-screen mx-auto">
<header class="text-white bg-gray-900 border-b">
<div class="flex items-center justify-between px-4 py-4 mx-auto">
<a href="<?= route_to('home') ?>" class="text-xl">Castopod Admin</a>
<nav>
<div class="flex items-center px-4 py-4 mx-auto">
<a href="<?= route_to('admin') ?>" class="text-xl">Castopod Admin</a>
<a href="<?= route_to(
'home'
) ?>" class="ml-4 text-sm underline hover:no-underline">Go to website</a>
<nav class="ml-auto">
<span class="mr-2">Welcome, <?= user()->username ?></span>
<a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to(
'logout'
@ -25,9 +28,8 @@
<div class="flex flex-1">
<?= view('admin/_sidenav') ?>
<main class="container flex-1 px-4 py-6 mx-auto">
<div class="mb-4">
<?= view('_message_block') ?>
</div>
<h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1>
<?= view('_message_block') ?>
<?= $this->renderSection('content') ?>
</main>
</div>

View File

@ -8,6 +8,11 @@
<div class="mb-4">
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Podcasts</span>
<ul>
<li>
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
'my_podcasts'
) ?>">My podcasts</a>
</li>
<li>
<a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
'podcast_list'

View File

@ -0,0 +1,48 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Contributor.add_contributor', [$podcast->title]) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<form action="<?= route_to(
'contributor_add',
$podcast->id
) ?>" method="post" class="flex flex-col max-w-lg">
<?= csrf_field() ?>
<div class="flex flex-col mb-4">
<label for="user"><?= lang('Contributor.form.user') ?></label>
<select id="user" name="user" autocomplete="off" class="form-select" required>
<?php foreach ($users as $user): ?>
<option value="<?= $user->id ?>"
<?php if (
old('user') == $user->id
): ?> selected <?php endif; ?>>
<?= $user->username ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-col mb-4">
<label for="role"><?= lang('Contributor.form.role') ?></label>
<select id="role" name="role" autocomplete="off" class="form-select" required>
<?php foreach ($roles as $role): ?>
<option value="<?= $role->id ?>"
<?php if (
old('role') == $role->id
): ?> selected <?php endif; ?>>
<?= $role->name ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
'Contributor.form.submit_add'
) ?></button>
</form>
<?= $this->endSection() ?>

View File

@ -0,0 +1,35 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Contributor.edit_role', [$user->username]) ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<form action="<?= route_to(
'contributor_edit',
$podcast->id,
$user->id
) ?>" method="post" class="flex flex-col max-w-lg">
<?= csrf_field() ?>
<div class="flex flex-col mb-4">
<label for="category"><?= lang('Contributor.form.role') ?></label>
<select id="role" name="role" autocomplete="off" class="form-select" required>
<?php foreach ($roles as $role): ?>
<option value="<?= $role->id ?>"
<?php if (
old('role') == $role->id
): ?> selected <?php endif; ?>>
<?= $role->name ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
'Contributor.form.submit_edit'
) ?></button>
</form>
<?= $this->endSection() ?>

View File

@ -0,0 +1,47 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Contributor.podcast_contributors') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
'contributor_add',
$podcast->id
) ?>"><?= lang('Contributor.add') ?></a>
<table class="table-auto">
<thead>
<tr>
<th class="px-4 py-2">Username</th>
<th class="px-4 py-2">Permissions</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
<?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">
<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',
$podcast->id,
$contributor->id
) ?>"><?= lang('Contributor.edit') ?></a>
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
'contributor_remove',
$podcast->id,
$contributor->id
) ?>"><?= lang('Contributor.remove') ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?= $this->endSection() ?>

View File

@ -1,8 +1,6 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('content') ?>
<h1 class="text-2xl">Welcome to the admin dashboard!</h1>
<?= $this->endSection() ?>
<?= $this->section(
'title'
) ?>Welcome to the admin dashboard!<?= $this->endSection() ?>

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Episode.create') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(route_to('episode_create', $podcast->name), [
<?= form_open_multipart(route_to('episode_create', $podcast->id), [
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]) ?>
@ -21,24 +20,30 @@
<div class="flex flex-col mb-4">
<label for="title"><?= lang('Episode.form.title') ?></label>
<input type="text" class="form-input" id="title" name="title" required />
<input type="text" class="form-input" id="title" name="title" required value="<?= old(
'title'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="slug"><?= lang('Episode.form.slug') ?></label>
<input type="text" class="form-input" id="slug" name="slug" required />
<input type="text" class="form-input" id="slug" name="slug" required value="<?= old(
'slug'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="description"><?= lang('Episode.form.description') ?></label>
<textarea class="form-textarea" id="description" name="description" required></textarea>
<textarea class="form-textarea" id="description" name="description" required><?= old(
'description'
) ?></textarea>
</div>
<div class="flex flex-col mb-4">
<label for="pub_date"><?= lang('Episode.form.pub_date') ?></label>
<input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date(
'Y-m-d'
) ?>" />
<input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= old(
'pub_date'
) || date('Y-m-d') ?>" />
</div>
<div class="flex flex-col mb-4">
@ -50,16 +55,22 @@
<label for="episode_number"><?= lang(
'Episode.form.episode_number'
) ?></label>
<input type="number" class="form-input" id="episode_number" name="episode_number" required />
<input type="number" class="form-input" id="episode_number" name="episode_number" required value="<?= old(
'episode_number'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="season_number"><?= lang('Episode.form.season_number') ?></label>
<input type="number" class="form-input" id="season_number" name="season_number" />
<input type="number" class="form-input" id="season_number" name="season_number" value="<?= old(
'season_number'
) ?>" />
</div>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if (
old('explicit')
): ?> checked <?php endif; ?> />
<label for="explicit" class="pl-2"><?= lang(
'Episode.form.explicit'
) ?></label>
@ -67,32 +78,45 @@
<div class="flex flex-col mb-4">
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
<input type="text" class="form-input" id="author_name" name="author_name" />
<input type="text" class="form-input" id="author_name" name="author_name" value="<?= old(
'author_name'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
<input type="email" class="form-input" id="author_email" name="author_email" />
<input type="email" class="form-input" id="author_email" name="author_email" value="<?= old(
'author_email'
) ?>" />
</div>
<fieldset class="flex flex-col mb-4">
<legend><?= lang('Episode.form.type.label') ?></legend>
<label for="full" class="inline-flex items-center">
<input type="radio" class="form-radio" value="full" id="full" name="type" required checked />
<input type="radio" class="form-radio" value="full" id="full" name="type" required <?php if (
!old('type') ||
old('type') == 'full'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Episode.form.type.full') ?></span>
</label>
<label for="trailer" class="inline-flex items-center">
<input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required />
<input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required <?php if (
old('type') == 'trailer'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>
</label>
<label for="bonus" class="inline-flex items-center">
<input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required />
<input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required <?php if (
old('type') == 'bonus'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>
</label>
</fieldset>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="block" name="block" class="form-checkbox" />
<input type="checkbox" id="block" name="block" class="form-checkbox" <?php if (
old('block')
): ?> checked <?php endif; ?> />
<label for="block" class="pl-2"><?= lang('Episode.form.block') ?></label>
</div>

View File

@ -1,20 +1,16 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Episode.edit') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(
route_to('episode_edit', $podcast->name, $episode->slug),
[
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]
) ?>
<?= form_open_multipart(route_to('episode_edit', $podcast->id, $episode->id), [
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]) ?>
<?= csrf_field() ?>
<div class="flex flex-col mb-4">

View File

@ -1,21 +1,29 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
'episode_create',
$podcast->id
) ?>"><?= lang('Episode.create') ?></a>
<div class="flex flex-col py-4">
<h1 class="mb-4 text-xl"><?= lang(
'Episode.all_podcast_episodes'
) ?> (<?= count($all_podcast_episodes) ?>)</h1>
<?php if ($all_podcast_episodes): ?>
<?php foreach ($all_podcast_episodes as $episode): ?>
<?php if ($podcast->episodes): ?>
<?php foreach ($podcast->episodes as $episode): ?>
<article class="flex-col w-full max-w-lg p-4 mb-4 border shadow">
<div class="flex mb-2">
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
<div class="flex flex-col flex-1">
<a href="<?= route_to(
'episode_edit',
$podcast->name,
$episode->slug
$podcast->id,
$episode->id
) ?>">
<h3 class="text-xl font-semibold">
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
@ -24,15 +32,15 @@
<p><?= $episode->description ?></p>
</a>
<audio controls class="mt-auto" preload="none">
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
<source src="<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
Your browser does not support the audio tag.
</audio>
</div>
</div>
<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
'episode_edit',
$podcast->name,
$episode->slug
$podcast->id,
$episode->id
) ?>"><?= lang('Episode.edit') ?></a>
<a href="<?= route_to(
'episode',
@ -43,21 +51,15 @@
) ?></a>
<a href="<?= route_to(
'episode_delete',
$podcast->name,
$episode->slug
$podcast->id,
$episode->id
) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
'Episode.delete'
) ?></a>
</article>
<?php endforeach; ?>
<?php else: ?>
<div class="flex items-center">
<p class="mr-4 italic"><?= lang('Podcast.no_episode') ?></p>
<a class="self-start px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
'episode_create',
$podcast->name
) ?>"><?= lang('Episode.create_one') ?></a>
</div>
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
<?php endif; ?>
</div>

View File

@ -1,8 +1,11 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('content') ?>
<?= $this->section('title') ?>
<?= lang('MyAccount.changePassword') ?>
<?= $this->endSection() ?>
<h1 class="mb-6 text-xl"><?= lang('MyAccount.changePassword') ?></h1>
<?= $this->section('content') ?>
<form action="<?= route_to(
'myAccount_changePassword'

View File

@ -1,5 +1,10 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('MyAccount.info') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Podcast.create') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(base_url(route_to('podcast_create')), [
<?= form_open_multipart(route_to('podcast_create'), [
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]) ?>
@ -16,24 +15,32 @@
<div class="flex flex-col mb-4">
<label for="title"><?= lang('Podcast.form.title') ?></label>
<input type="text" class="form-input" id="title" name="title" required />
<input type="text" class="form-input" id="title" name="title" value="<?= old(
'title'
) ?>" required />
</div>
<div class="flex flex-col mb-4">
<label for="name"><?= lang('Podcast.form.name') ?></label>
<input type="text" class="form-input" id="name" name="name" required />
<input type="text" class="form-input" id="name" name="name" value="<?= old(
'name'
) ?>" required />
</div>
<div class="flex flex-col mb-4">
<label for="description"><?= lang('Podcast.form.description') ?></label>
<textarea class="form-textarea" id="description" name="description" required></textarea>
<textarea class="form-textarea" id="description" name="description" required><?= old(
'description'
) ?></textarea>
</div>
<div class="flex flex-col mb-4">
<label for="episode_description_footer"><?= lang(
'Podcast.form.episode_description_footer'
) ?></label>
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"></textarea>
<textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"><?= old(
'episode_description_footer'
) ?></textarea>
</div>
<div class="flex flex-col mb-4">
@ -45,9 +52,15 @@
<label for="language"><?= lang('Podcast.form.language') ?></label>
<select id="language" name="language" autocomplete="off" class="form-select" required>
<?php foreach ($languages as $language): ?>
<option <?= $language->code == $browser_lang
? "selected='selected'"
: '' ?> value="<?= $language->code ?>">
<option value="<?= $language->code ?>"
<?php if (
old('language') == $language->code
): ?> selected <?php endif; ?>
<?php if (
!old('language') &&
$language->code == $browser_lang
): ?> selected <?php endif; ?>
>
<?= $language->native_name ?>
</option>
<?php endforeach; ?>
@ -58,16 +71,20 @@
<label for="category"><?= lang('Podcast.form.category') ?></label>
<select id="category" name="category" class="form-select" required>
<?php foreach ($categories as $category): ?>
<option value="<?= $category->code ?>"><?= lang(
'Podcast.category_options.' . $category->code
) ?>
<option value="<?= $category->code ?>"
<?php if (
old('category') == $category->code
): ?> selected <?php endif; ?>
><?= lang('Podcast.category_options.' . $category->code) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if (
old('explicit')
): ?> checked <?php endif; ?> />
<label for="explicit" class="pl-2"><?= lang(
'Podcast.form.explicit'
) ?></label>
@ -75,48 +92,67 @@
<div class="flex flex-col mb-4">
<label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
<input type="text" class="form-input" id="author_name" name="author_name" />
<input type="text" class="form-input" id="author_name" name="author_name" value="<?= old(
'author_name'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
<input type="email" class="form-input" id="author_email" name="author_email" />
<input type="email" class="form-input" id="author_email" name="author_email" value="<?= old(
'author_email'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="owner_name"><?= lang('Podcast.form.owner_name') ?></label>
<input type="text" class="form-input" id="owner_name" name="owner_name" />
<input type="text" class="form-input" id="owner_name" name="owner_name" value="<?= old(
'owner_name'
) ?>" />
</div>
<div class="flex flex-col mb-4">
<label for="owner_email"><?= lang('Podcast.form.owner_email') ?></label>
<input type="email" class="form-input" id="owner_email" name="owner_email" required />
<input type="email" class="form-input" id="owner_email" name="owner_email" value="<?= old(
'owner_email'
) ?>" required />
</div>
<fieldset class="flex flex-col mb-4">
<legend><?= lang('Podcast.form.type.label') ?></legend>
<label for="episodic" class="inline-flex items-center">
<input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required checked />
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
<input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required <?php if (
!old('type') ||
old('type') == 'episodic'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
</label>
<label for="serial" class="inline-flex items-center">
<input type="radio" class="form-radio" value="serial" id="serial" name="type" required />
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
<input type="radio" class="form-radio" value="serial" id="serial" name="type" required <?php if (
old('type') == 'serial'
): ?> checked <?php endif; ?> />
<span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
</label>
</fieldset>
<div class="flex flex-col mb-4">
<label for="copyright"><?= lang('Podcast.form.copyright') ?></label>
<input type="text" class="form-input" id="copyright" name="copyright" />
<input type="text" class="form-input" id="copyright" name="copyright" value="<?= old(
'copyright'
) ?>" />
</div>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="block" name="block" class="form-checkbox" />
<input type="checkbox" id="block" name="block" class="form-checkbox" <?php if (
old('block')
): ?> checked <?php endif; ?> />
<label for="block" class="pl-2"><?= lang('Podcast.form.block') ?></label>
</div>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="complete" name="complete" class="form-checkbox" />
<input type="checkbox" id="complete" name="complete" class="form-checkbox" <?php if (
old('complete')
): ?> checked <?php endif; ?> />
<label for="complete" class="pl-2"><?= lang(
'Podcast.form.complete'
) ?></label>

View File

@ -1,14 +1,13 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('Podcast.edit') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<?= form_open_multipart(base_url(route_to('podcast_edit', $podcast->name)), [
<?= form_open_multipart(route_to('podcast_edit', $podcast->id), [
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]) ?>
@ -70,7 +69,9 @@
</div>
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" checked="<?= $podcast->explicit ?>" />
<input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?= $podcast->explicit
? 'checked'
: '' ?> />
<label for="explicit" class="pl-2"><?= lang(
'Podcast.form.explicit'
) ?></label>
@ -123,7 +124,7 @@
<div class="inline-flex items-center mb-4">
<input type="checkbox" id="complete" name="complete" class="form-checkbox"
<?= $podcast->block ? 'checked' : '' ?> />
<?= $podcast->complete ? 'checked' : '' ?> />
<label for="complete" class="pl-2"><?= lang(
'Podcast.form.complete'
) ?></label>

View File

@ -1,10 +1,15 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.all_podcasts') ?> (<?= count($all_podcasts) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-2 text-xl"><?= lang('Podcast.all_podcasts') ?> (<?= count(
$all_podcasts
) ?>)</h1>
<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
'podcast_create'
) ?>"><?= lang('Podcast.create') ?></a>
<div class="flex flex-wrap">
<?php if ($all_podcasts): ?>
<?php foreach ($all_podcasts as $podcast): ?>
@ -12,36 +17,36 @@
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
<a href="<?= route_to(
'episode_list',
$podcast->name
$podcast->id
) ?>" class="hover:underline">
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
</a>
<p class="mb-4 text-gray-600">@<?= $podcast->name ?></p>
<a class="inline-flex px-2 py-1 mb-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
'podcast_edit',
$podcast->name
$podcast->id
) ?>"><?= lang('Podcast.edit') ?></a>
<a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to(
'episode_list',
$podcast->name
$podcast->id
) ?>"><?= lang('Podcast.see_episodes') ?></a>
<a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to(
'contributor_list',
$podcast->id
) ?>"><?= lang('Podcast.see_contributors') ?></a>
<a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
'podcast',
$podcast->name
) ?>"><?= lang('Podcast.goto_page') ?></a>
<a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
'podcast_delete',
$podcast->name
$podcast->id
) ?>"><?= lang('Podcast.delete') ?></a>
</article>
<?php endforeach; ?>
<?php else: ?>
<div class="flex items-center">
<p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p>
<a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to(
'podcast_create'
) ?>"><?= lang('Podcast.create_one') ?></a>
</div>
<p class="italic"><?= lang('Podcast.no_podcast') ?></p>
<?php endif; ?>
</div>

View File

@ -1,13 +1,12 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('User.create') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<h1 class="mb-6 text-xl"><?= lang('User.create') ?></h1>
<div class="mb-8">
<?= \Config\Services::validation()->listErrors() ?>
</div>
<form action="<?= route_to(
'user_create'
) ?>" method="post" class="flex flex-col max-w-lg">

View File

@ -1,61 +1,55 @@
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= lang('User.all_users') ?> (<?= count($all_users) ?>)
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="flex flex-wrap">
<?php if ($all_users): ?>
<table class="table-auto">
<thead>
<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">Banned?</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($all_users as $user): ?>
<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"><?= $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(
'user_force_pass_reset',
$user->username
) ?>"><?= lang('User.forcePassReset') ?></a>
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
$user->isBanned() ? 'user_unban' : 'user_ban',
$user->username
) ?>">
<?= $user->isBanned()
? lang('User.unban')
: lang('User.ban') ?></a>
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
'user_delete',
$user->username
) ?>"><?= lang('User.delete') ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="flex items-center">
<p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p>
<a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to(
'podcast_create'
) ?>"><?= lang('Podcast.create_one') ?></a>
</div>
<?php endif; ?>
</div>
<table class="table-auto">
<thead>
<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">Banned?</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($all_users as $user): ?>
<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"><?= $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(
'user_force_pass_reset',
$user->id
) ?>"><?= lang('User.forcePassReset') ?></a>
<a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
$user->isBanned() ? 'user_unban' : 'user_ban',
$user->id
) ?>">
<?= $user->isBanned()
? lang('User.unban')
: lang('User.ban') ?></a>
<a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
'user_delete',
$user->id
) ?>"><?= lang('User.delete') ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?= $this->endSection()
?>

View File

@ -17,9 +17,7 @@
) ?></a>
</header>
<main class="w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg shadow">
<div class="mb-4">
<?= view('_message_block') ?>
</div>
<?= view('_message_block') ?>
<?= $this->renderSection('content') ?>
</main>
<footer class="flex flex-col text-sm">

View File

@ -6,7 +6,7 @@
"license": "AGPL-3.0-or-later",
"require": {
"php": ">=7.2",
"codeigniter4/framework": "^4",
"codeigniter4/framework": "4.0.3",
"james-heinrich/getid3": "~2.0.0-dev",
"whichbrowser/parser": "^2.0",
"geoip2/geoip2": "~2.0",

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a03d5be6665057254fa301cada96586e",
"content-hash": "e494a281a4c6a239790ea930d05764e2",
"packages": [
{
"name": "codeigniter4/framework",
@ -1158,33 +1158,33 @@
},
{
"name": "phpspec/prophecy",
"version": "v1.10.3",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "451c3cd1418cf640de218914901e51b064abb093"
"reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
"reference": "451c3cd1418cf640de218914901e51b064abb093",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
"reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
"sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
"doctrine/instantiator": "^1.2",
"php": "^7.2",
"phpdocumentor/reflection-docblock": "^5.0",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5 || ^3.2",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
"phpspec/phpspec": "^6.0",
"phpunit/phpunit": "^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10.x-dev"
"dev-master": "1.11.x-dev"
}
},
"autoload": {
@ -1217,7 +1217,7 @@
"spy",
"stub"
],
"time": "2020-03-05T15:02:03+00:00"
"time": "2020-07-08T12:44:21+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -2181,16 +2181,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.17.1",
"version": "v1.18.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
"reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
"shasum": ""
},
"require": {
@ -2202,7 +2202,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.17-dev"
"dev-master": "1.18-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2253,27 +2253,27 @@
"type": "tidelift"
}
],
"time": "2020-06-06T08:46:27+00:00"
"time": "2020-07-14T12:35:20+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.1.3",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
"reference": "75a63c33a8577608444246075ea0af0d052e452a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
"reference": "75a63c33a8577608444246075ea0af0d052e452a",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": "^7.0"
"php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@ -2293,24 +2293,30 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"time": "2019-06-13T22:48:21+00:00"
"funding": [
{
"url": "https://github.com/theseer",
"type": "github"
}
],
"time": "2020-07-12T23:59:07+00:00"
},
{
"name": "webmozart/assert",
"version": "1.9.0",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "9dc4f203e36f2b486149058bade43c851dd97451"
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451",
"reference": "9dc4f203e36f2b486149058bade43c851dd97451",
"url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0",
"php": "^5.3.3 || ^7.0 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
@ -2342,7 +2348,7 @@
"check",
"validate"
],
"time": "2020-06-16T10:16:42+00:00"
"time": "2020-07-08T17:02:28+00:00"
}
],
"aliases": [],