feat(users): add myth-auth to handle users crud + add admin gateway only accessible by login
- overwrite myth/auth config with castopod app needs - create custom views for users authentication - add admin area bootstrapped by admin controller - shift podcast and episodes crud to admin area - reorganize view layouts - update docs for database migration - add myth-auth to DEPENDENCIES.md closes #11
This commit is contained in:
parent
da0f047281
commit
c63a077618
|
@ -13,3 +13,4 @@ Castopod uses the following components:
|
|||
- [GeoIP2 PHP API](https://github.com/maxmind/GeoIP2-php) ([Apache License 2.0](https://github.com/maxmind/GeoIP2-php/blob/master/LICENSE))
|
||||
- [Quill Rich Text Editor](https://github.com/quilljs/quill) ([BSD 3-Clause "New" or "Revised" License](https://github.com/quilljs/quill/blob/develop/LICENSE))
|
||||
- [getID3](https://github.com/JamesHeinrich/getID3) ([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt))
|
||||
- [myth-auth](https://github.com/lonnieezell/myth-auth) ([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md))
|
||||
|
|
|
@ -274,4 +274,20 @@ class App extends BaseConfig
|
|||
| Defines the root folder for media files storage
|
||||
*/
|
||||
public $mediaRoot = 'media';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Admin gateway
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines a base route for all admin pages
|
||||
*/
|
||||
public $adminGateway = 'admin';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auth gateway
|
||||
|--------------------------------------------------------------------------
|
||||
| Defines a base route for all authentication related pages
|
||||
*/
|
||||
public $authGateway = 'auth';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
class Auth extends \Myth\Auth\Config\Auth
|
||||
{
|
||||
//--------------------------------------------------------------------
|
||||
// Views used by Auth Controllers
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
public $views = [
|
||||
'login' => 'auth/login',
|
||||
'register' => 'auth/register',
|
||||
'forgot' => 'auth/forgot',
|
||||
'reset' => 'auth/reset',
|
||||
'emailForgot' => 'auth/emails/forgot',
|
||||
'emailActivation' => 'auth/emails/activation',
|
||||
];
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Layout for the views to extend
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
public $viewLayout = 'auth/_layout';
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Allow User Registration
|
||||
//--------------------------------------------------------------------
|
||||
// When enabled (default) any unregistered user may apply for a new
|
||||
// account. If you disable registration you may need to ensure your
|
||||
// controllers and views know not to offer registration.
|
||||
//
|
||||
public $allowRegistration = false;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Require confirmation registration via email
|
||||
//--------------------------------------------------------------------
|
||||
// When enabled, every registered user will receive an email message
|
||||
// with a special link he have to confirm to activate his account.
|
||||
//
|
||||
public $requireActivation = false;
|
||||
}
|
|
@ -10,6 +10,9 @@ class Filters extends BaseConfig
|
|||
'csrf' => \CodeIgniter\Filters\CSRF::class,
|
||||
'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
|
||||
'honeypot' => \CodeIgniter\Filters\Honeypot::class,
|
||||
'login' => \Myth\Auth\Filters\LoginFilter::class,
|
||||
'role' => \Myth\Auth\Filters\RoleFilter::class,
|
||||
'permission' => \Myth\Auth\Filters\PermissionFilter::class,
|
||||
];
|
||||
|
||||
// Always applied before every request
|
||||
|
@ -33,4 +36,13 @@ class Filters extends BaseConfig
|
|||
// that they should run on, like:
|
||||
// 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']],
|
||||
public $filters = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->filters = [
|
||||
'login' => ['before' => [config('App')->adminGateway . '*']],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ $routes->set404Override();
|
|||
$routes->setAutoRoute(false);
|
||||
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}');
|
||||
$routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}');
|
||||
$routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}');
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
|
@ -34,28 +35,13 @@ $routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}');
|
|||
// We get a performance increase by specifying the default
|
||||
// route since we don't have to scan directories.
|
||||
$routes->get('/', 'Home::index', ['as' => 'home']);
|
||||
$routes->add('new-podcast', 'Podcast::create', ['as' => 'podcast_create']);
|
||||
|
||||
$routes->group('@(:podcastName)', function ($routes) {
|
||||
$routes->add('/', 'Podcast::view/$1', ['as' => 'podcast_view']);
|
||||
$routes->add('edit', 'Podcast::edit/$1', [
|
||||
'as' => 'podcast_edit',
|
||||
]);
|
||||
$routes->add('delete', 'Podcast::delete/$1', [
|
||||
'as' => 'podcast_delete',
|
||||
]);
|
||||
$routes->add('/', 'Podcast/$1', ['as' => 'podcast']);
|
||||
|
||||
$routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
|
||||
$routes->add('new-episode', 'Episode::create/$1', [
|
||||
'as' => 'episode_create',
|
||||
]);
|
||||
$routes->add('episodes/(:episodeSlug)', 'Episode::view/$1/$2', [
|
||||
'as' => 'episode_view',
|
||||
]);
|
||||
$routes->add('episodes/(:episodeSlug)/edit', 'Episode::edit/$1/$2', [
|
||||
'as' => 'episode_edit',
|
||||
]);
|
||||
$routes->add('episodes/(:episodeSlug)/delete', 'Episode::delete/$1/$2', [
|
||||
'as' => 'episode_delete',
|
||||
$routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [
|
||||
'as' => 'episode',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -68,6 +54,132 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
|
|||
$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents');
|
||||
$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
|
||||
|
||||
// Admin area
|
||||
$routes->group(
|
||||
config('App')->adminGateway,
|
||||
['namespace' => 'App\Controllers\Admin'],
|
||||
function ($routes) {
|
||||
$routes->add('/', 'Home', [
|
||||
'as' => 'admin',
|
||||
]);
|
||||
|
||||
$routes->add('new-podcast', 'Podcast::create', [
|
||||
'as' => 'podcast_create',
|
||||
]);
|
||||
$routes->add('podcasts', 'Podcast::list', ['as' => 'podcast_list']);
|
||||
|
||||
$routes->group('podcasts/@(:podcastName)', function ($routes) {
|
||||
$routes->add('edit', 'Podcast::edit/$1', [
|
||||
'as' => 'podcast_edit',
|
||||
]);
|
||||
$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', [
|
||||
'as' => 'episode_list',
|
||||
]);
|
||||
|
||||
$routes->add(
|
||||
'episodes/(:episodeSlug)/edit',
|
||||
'Episode::edit/$1/$2',
|
||||
[
|
||||
'as' => 'episode_edit',
|
||||
]
|
||||
);
|
||||
$routes->add(
|
||||
'episodes/(:episodeSlug)/delete',
|
||||
'Episode::delete/$1/$2',
|
||||
[
|
||||
'as' => 'episode_delete',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// 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->add('users/@(:any)/unban', 'User::unBan/$1', [
|
||||
'as' => 'user_unban',
|
||||
]);
|
||||
$routes->add(
|
||||
'users/@(:any)/force-pass-reset',
|
||||
'User::forcePassReset/$1',
|
||||
[
|
||||
'as' => 'user_force_pass_reset',
|
||||
]
|
||||
);
|
||||
|
||||
$routes->add('users/@(:any)/delete', 'User::delete/$1', [
|
||||
'as' => 'user_delete',
|
||||
]);
|
||||
|
||||
// My account
|
||||
$routes->get('my-account', 'Myaccount', [
|
||||
'as' => 'myAccount',
|
||||
]);
|
||||
$routes->get(
|
||||
'my-account/change-password',
|
||||
'Myaccount::changePassword/$1',
|
||||
[
|
||||
'as' => 'myAccount_change-password',
|
||||
]
|
||||
);
|
||||
$routes->post(
|
||||
'my-account/change-password',
|
||||
'Myaccount::attemptChange/$1',
|
||||
[
|
||||
'as' => 'myAccount_change-password',
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Overwriting Myth:auth routes file
|
||||
*/
|
||||
$routes->group(config('App')->authGateway, function ($routes) {
|
||||
// Login/out
|
||||
$routes->get('login', 'Auth::login', ['as' => 'login']);
|
||||
$routes->post('login', 'Auth::attemptLogin');
|
||||
$routes->get('logout', 'Auth::logout', ['as' => 'logout']);
|
||||
|
||||
// Registration
|
||||
$routes->get('register', 'Auth::register', [
|
||||
'as' => 'register',
|
||||
]);
|
||||
$routes->post('register', 'Auth::attemptRegister');
|
||||
|
||||
// Activation
|
||||
$routes->get('activate-account', 'Auth::activateAccount', [
|
||||
'as' => 'activate-account',
|
||||
]);
|
||||
$routes->get('resend-activate-account', 'Auth::resendActivateAccount', [
|
||||
'as' => 'resend-activate-account',
|
||||
]);
|
||||
|
||||
// Forgot/Resets
|
||||
$routes->get('forgot', 'Auth::forgotPassword', [
|
||||
'as' => 'forgot',
|
||||
]);
|
||||
$routes->post('forgot', 'Auth::attemptForgot');
|
||||
$routes->get('reset-password', 'Auth::resetPassword', [
|
||||
'as' => 'reset-password',
|
||||
]);
|
||||
$routes->post('reset-password', 'Auth::attemptReset');
|
||||
$routes->get('change-password', 'Auth::changePassword', [
|
||||
'as' => 'change_pass',
|
||||
]);
|
||||
$routes->post('change-password', 'Auth::attemptChange');
|
||||
});
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* Additional Routing
|
||||
|
|
|
@ -25,6 +25,7 @@ class Toolbar extends BaseConfig
|
|||
\CodeIgniter\Debug\Toolbar\Collectors\Files::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Routes::class,
|
||||
\CodeIgniter\Debug\Toolbar\Collectors\Events::class,
|
||||
\Myth\Auth\Collectors\Auth::class,
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
|
@ -17,6 +17,7 @@ class Validation
|
|||
\CodeIgniter\Validation\FormatRules::class,
|
||||
\CodeIgniter\Validation\FileRules::class,
|
||||
\CodeIgniter\Validation\CreditCardRules::class,
|
||||
\Myth\Auth\Authentication\Passwords\ValidationRules::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
* BaseController provides a convenient place for loading components
|
||||
* and performing functions that are needed by all your controllers.
|
||||
* Extend this class in any new controllers:
|
||||
* class Home extends BaseController
|
||||
*
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
*
|
||||
* @package CodeIgniter
|
||||
*/
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
* An array of helpers to be loaded automatically upon
|
||||
* class instantiation. These helpers will be available
|
||||
* to all other controllers that extend BaseController.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['auth'];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function initController(
|
||||
\CodeIgniter\HTTP\RequestInterface $request,
|
||||
\CodeIgniter\HTTP\ResponseInterface $response,
|
||||
\Psr\Log\LoggerInterface $logger
|
||||
) {
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// Preload any models, libraries, etc, here.
|
||||
//--------------------------------------------------------------------
|
||||
// E.g.:
|
||||
// $this->session = \Config\Services::session();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
<?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\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
|
||||
class Episode extends BaseController
|
||||
{
|
||||
protected \App\Entities\Podcast $podcast;
|
||||
protected ?\App\Entities\Episode $episode;
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
$podcast_model = new PodcastModel();
|
||||
|
||||
$this->podcast = $podcast_model->where('name', $params[0])->first();
|
||||
|
||||
if (count($params) > 1) {
|
||||
$episode_model = new EpisodeModel();
|
||||
if (
|
||||
!($episode = $episode_model
|
||||
->where([
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'slug' => $params[1],
|
||||
])
|
||||
->first())
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
$this->episode = $episode;
|
||||
}
|
||||
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
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,
|
||||
]);
|
||||
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->save($new_episode);
|
||||
|
||||
return redirect()->route('episode_list', [$this->podcast->name]);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->save($this->episode);
|
||||
|
||||
return redirect()->route('episode_list', [$this->podcast->name]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->delete($this->episode->id);
|
||||
|
||||
return redirect()->route('episode_list', [$this->podcast->name]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers\Admin;
|
||||
|
||||
class Home extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('admin/dashboard');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?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 Myth\Auth\Config\Services;
|
||||
use Myth\Auth\Models\UserModel;
|
||||
|
||||
class Myaccount extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('admin/my_account/view');
|
||||
}
|
||||
|
||||
public function changePassword()
|
||||
{
|
||||
return view('admin/my_account/change_password');
|
||||
}
|
||||
|
||||
public function attemptChange()
|
||||
{
|
||||
$auth = Services::authentication();
|
||||
$user_model = new UserModel();
|
||||
|
||||
// Validate here first, since some things,
|
||||
// like the password, can only be validated properly here.
|
||||
$rules = [
|
||||
'email' => 'required|valid_email',
|
||||
'password' => 'required',
|
||||
'new_password' => 'required|strong_password',
|
||||
'new_pass_confirm' => 'required|matches[new_password]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
$credentials = [
|
||||
'email' => user()->email,
|
||||
'password' => $this->request->getPost('password'),
|
||||
];
|
||||
|
||||
if (!$auth->validate($credentials)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
user()->password = $this->request->getPost('new_password');
|
||||
$user_model->save(user());
|
||||
|
||||
if (!$user_model->save(user())) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
// Success!
|
||||
return redirect()
|
||||
->route('myAccount')
|
||||
->with('message', lang('MyAccount.passwordChangeSuccess'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
<?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\Entities\UserPodcast;
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\LanguageModel;
|
||||
use App\Models\PodcastModel;
|
||||
|
||||
class Podcast extends BaseController
|
||||
{
|
||||
protected ?\App\Entities\Podcast $podcast;
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (count($params) > 0) {
|
||||
$podcast_model = new PodcastModel();
|
||||
if (
|
||||
!($podcast = $podcast_model->where('name', $params[0])->first())
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
$this->podcast = $podcast;
|
||||
}
|
||||
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
$podcast_model = new PodcastModel();
|
||||
|
||||
$data = ['all_podcasts' => $podcast_model->findAll()];
|
||||
|
||||
return view('admin/podcast/list', $data);
|
||||
}
|
||||
|
||||
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')
|
||||
),
|
||||
];
|
||||
|
||||
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'
|
||||
),
|
||||
]);
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
helper(['form', 'misc']);
|
||||
|
||||
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(),
|
||||
];
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
$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'
|
||||
);
|
||||
|
||||
$podcast_model = new PodcastModel();
|
||||
$podcast_model->save($this->podcast);
|
||||
|
||||
return redirect()->route('podcast_list', [$this->podcast->name]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$podcast_model = new PodcastModel();
|
||||
$podcast_model->delete($this->podcast->id);
|
||||
|
||||
return redirect()->route('podcast_list');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?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 Myth\Auth\Models\UserModel;
|
||||
|
||||
class User extends BaseController
|
||||
{
|
||||
protected ?\Myth\Auth\Entities\User $user;
|
||||
|
||||
public function _remap($method, ...$params)
|
||||
{
|
||||
if (count($params) > 0) {
|
||||
$user_model = new UserModel();
|
||||
if (
|
||||
!($user = $user_model->where('username', $params[0])->first())
|
||||
) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
public function list()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
|
||||
$data = ['all_users' => $user_model->findAll()];
|
||||
|
||||
return view('admin/user/list', $data);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
|
||||
// Validate here first, since some things,
|
||||
// like the password, can only be validated properly here.
|
||||
$rules = array_merge(
|
||||
$user_model->getValidationRules(['only' => ['username']]),
|
||||
[
|
||||
'email' => 'required|valid_email|is_unique[users.email]',
|
||||
'password' => 'required|strong_password',
|
||||
'pass_confirm' => 'required|matches[password]',
|
||||
]
|
||||
);
|
||||
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
public function forcePassReset()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
|
||||
$this->user->force_pass_reset = true;
|
||||
$this->user->generateResetHash();
|
||||
|
||||
if (!$user_model->save($this->user)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
// Success!
|
||||
return redirect()
|
||||
->route('user_list')
|
||||
->with('message', lang('User.forcePassResetSuccess'));
|
||||
}
|
||||
|
||||
public function ban()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
$this->user->ban('');
|
||||
|
||||
if (!$user_model->save($this->user)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('user_list')
|
||||
->with('message', lang('User.banSuccess'));
|
||||
}
|
||||
|
||||
public function unBan()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
$this->user->unBan();
|
||||
|
||||
if (!$user_model->save($this->user)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->with('errors', $user_model->errors());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('user_list')
|
||||
->with('message', lang('User.unbanSuccess'));
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$user_model = new UserModel();
|
||||
$user_model->delete($this->user->id);
|
||||
|
||||
return redirect()
|
||||
->route('user_list')
|
||||
->with('message', lang('User.deleteSuccess'));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<?php namespace App\Controllers;
|
||||
<?php
|
||||
/**
|
||||
* Class Analytics
|
||||
* Creates Analytics controller
|
||||
|
@ -7,6 +7,8 @@
|
|||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class Analytics extends Controller
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Myth\Auth\Models\UserModel;
|
||||
|
||||
class Auth extends \Myth\Auth\Controllers\AuthController
|
||||
{
|
||||
/**
|
||||
* An array of helpers to be loaded automatically upon
|
||||
* class instantiation. These helpers will be available
|
||||
* to all other controllers that extend BaseController.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $helpers = ['auth'];
|
||||
|
||||
/**
|
||||
* Displays the login form, or redirects
|
||||
* the user to their destination/home if
|
||||
* they are already logged in.
|
||||
*/
|
||||
public function changePassword()
|
||||
{
|
||||
return view('auth/change_password', [
|
||||
'config' => $this->config,
|
||||
'email' => user()->email,
|
||||
'token' => user()->reset_hash,
|
||||
]);
|
||||
}
|
||||
|
||||
public function attemptChange()
|
||||
{
|
||||
$users = new UserModel();
|
||||
|
||||
// First things first - log the reset attempt.
|
||||
$users->logResetAttempt(
|
||||
$this->request->getPost('email'),
|
||||
$this->request->getPost('token'),
|
||||
$this->request->getIPAddress(),
|
||||
(string) $this->request->getUserAgent()
|
||||
);
|
||||
|
||||
$rules = [
|
||||
'token' => 'required',
|
||||
'email' => 'required|valid_email',
|
||||
'password' => 'required|strong_password',
|
||||
'pass_confirm' => 'required|matches[password]',
|
||||
];
|
||||
|
||||
if (!$this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $users->errors());
|
||||
}
|
||||
|
||||
$user = $users
|
||||
->where('email', $this->request->getPost('email'))
|
||||
->where('reset_hash', $this->request->getPost('token'))
|
||||
->first();
|
||||
|
||||
if (is_null($user)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', lang('Auth.forgotNoUser'));
|
||||
}
|
||||
|
||||
// Reset token still valid?
|
||||
if (
|
||||
!empty($user->reset_expires) &&
|
||||
time() > $user->reset_expires->getTimestamp()
|
||||
) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('error', lang('Auth.resetTokenExpired'));
|
||||
}
|
||||
|
||||
// Success! Save the new password, and cleanup the reset hash.
|
||||
$user->password = $this->request->getPost('password');
|
||||
$user->reset_hash = null;
|
||||
$user->reset_at = date('Y-m-d H:i:s');
|
||||
$user->reset_expires = null;
|
||||
$user->force_pass_reset = false;
|
||||
$users->save($user);
|
||||
|
||||
return redirect()
|
||||
->route('login')
|
||||
->with('message', lang('Auth.resetSuccess'));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
|
@ -15,6 +13,8 @@ namespace App\Controllers;
|
|||
* @package CodeIgniter
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class BaseController extends Controller
|
||||
|
|
|
@ -39,130 +39,7 @@ class Episode extends BaseController
|
|||
return $this->$method();
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
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('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')
|
||||
? $this->request->getVar('season_number')
|
||||
: null,
|
||||
'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,
|
||||
]);
|
||||
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->save($new_episode);
|
||||
|
||||
return redirect()->to(
|
||||
base_url(
|
||||
route_to(
|
||||
'episode_view',
|
||||
$this->podcast->name,
|
||||
$new_episode->slug
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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('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');
|
||||
}
|
||||
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->save($this->episode);
|
||||
|
||||
return redirect()->to(
|
||||
base_url(
|
||||
route_to(
|
||||
'episode_view',
|
||||
$this->podcast->name,
|
||||
$this->episode->slug
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function view()
|
||||
public function index()
|
||||
{
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
$this->cachePage(DECADE);
|
||||
|
@ -173,16 +50,6 @@ class Episode extends BaseController
|
|||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
return view('episode/view', $data);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$episode_model = new EpisodeModel();
|
||||
$episode_model->delete($this->episode->id);
|
||||
|
||||
return redirect()->to(
|
||||
base_url(route_to('podcast_view', $this->podcast->name))
|
||||
);
|
||||
return view('episode', $data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
|
|
|
@ -19,9 +19,7 @@ class Home extends BaseController
|
|||
|
||||
// check if there's only one podcast to redirect user to it
|
||||
if (count($all_podcasts) == 1) {
|
||||
return redirect()->to(
|
||||
base_url(route_to('podcast_view', $all_podcasts[0]->name))
|
||||
);
|
||||
return redirect()->route('podcast', [$all_podcasts[0]->name]);
|
||||
}
|
||||
|
||||
// default behavior: list all podcasts on home page
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
*/
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\CategoryModel;
|
||||
use App\Models\LanguageModel;
|
||||
use App\Models\PodcastModel;
|
||||
|
||||
class Podcast extends BaseController
|
||||
|
@ -29,131 +27,7 @@ class Podcast extends BaseController
|
|||
return $this->$method();
|
||||
}
|
||||
|
||||
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')
|
||||
),
|
||||
];
|
||||
|
||||
echo view('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'
|
||||
),
|
||||
]);
|
||||
|
||||
$podcast_model->save($podcast);
|
||||
|
||||
return redirect()->to(
|
||||
base_url(route_to('podcast_view', $podcast->name))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function edit()
|
||||
{
|
||||
helper(['form', 'misc']);
|
||||
|
||||
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(),
|
||||
];
|
||||
|
||||
echo view('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'
|
||||
);
|
||||
|
||||
$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'
|
||||
);
|
||||
|
||||
$podcast_model = new PodcastModel();
|
||||
$podcast_model->save($this->podcast);
|
||||
|
||||
return redirect()->to(
|
||||
base_url(route_to('podcast_view', $this->podcast->name))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function view()
|
||||
public function index()
|
||||
{
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
$this->cachePage(DECADE);
|
||||
|
@ -164,14 +38,6 @@ class Podcast extends BaseController
|
|||
'podcast' => $this->podcast,
|
||||
'episodes' => $this->podcast->episodes,
|
||||
];
|
||||
return view('podcast/view', $data);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$podcast_model = new PodcastModel();
|
||||
$podcast_model->delete($this->podcast->id);
|
||||
|
||||
return redirect()->to(base_url(route_to('home')));
|
||||
return view('podcast', $data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
<?php namespace App\Controllers;
|
||||
<?php
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class UnknownUserAgents extends Controller
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/**
|
||||
* Class AddLanguages
|
||||
* Creates languages table in database
|
||||
*
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
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,
|
||||
'unsigned' => true,
|
||||
],
|
||||
'podcast_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey(['user_id', 'podcast_id']);
|
||||
$this->forge->addForeignKey('user_id', 'users', 'id');
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||
$this->forge->createTable('users_podcasts');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('users_podcasts');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -135,7 +135,7 @@ class Episode extends Entity
|
|||
{
|
||||
return base_url(
|
||||
route_to(
|
||||
'episode_view',
|
||||
'episode',
|
||||
$this->getPodcast()->name,
|
||||
$this->attributes['slug']
|
||||
)
|
||||
|
|
|
@ -71,7 +71,7 @@ class Podcast extends Entity
|
|||
|
||||
public function getLink()
|
||||
{
|
||||
return base_url(route_to('podcast_view', $this->attributes['name']));
|
||||
return base_url(route_to('podcast', $this->attributes['name']));
|
||||
}
|
||||
|
||||
public function getFeedUrl()
|
||||
|
@ -79,12 +79,25 @@ class Podcast extends Entity
|
|||
return base_url(route_to('podcast_feed', $this->attributes['name']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the podcast's episodes
|
||||
*
|
||||
* @return \App\Entities\Episode[]
|
||||
*/
|
||||
public function getEpisodes()
|
||||
{
|
||||
$episode_model = new EpisodeModel();
|
||||
if (empty($this->id)) {
|
||||
throw new \RuntimeException(
|
||||
'Podcast must be created before getting episodes.'
|
||||
);
|
||||
}
|
||||
|
||||
return $episode_model
|
||||
->where('podcast_id', $this->attributes['id'])
|
||||
->findAll();
|
||||
if (empty($this->permissions)) {
|
||||
$this->episodes = (new EpisodeModel())->getPodcastEpisodes(
|
||||
$this->id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->episodes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?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',
|
||||
];
|
||||
}
|
|
@ -13,7 +13,6 @@ function set_user_session_country()
|
|||
{
|
||||
$session = \Config\Services::session();
|
||||
$session->start();
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$country = 'N/A';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
/**
|
||||
* Saves a file to the corresponding podcast folder in `public/media`
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @param \CodeIgniter\HTTP\Files\UploadedFile $file
|
||||
* @param string $podcast_name
|
||||
* @param string $file_name
|
||||
*
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'create_one' => 'Add a new one',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'edit' => 'Edit',
|
||||
'delete' => 'Delete',
|
||||
'goto_page' => 'Go to page',
|
||||
'create' => 'Add an episode',
|
||||
'form' => [
|
||||
'file' => 'Audio file',
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'all_podcasts' => 'All podcasts',
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'passwordChangeSuccess' => 'Password has been successfully changed!',
|
||||
'changePassword' => 'Change my password'
|
||||
];
|
|
@ -1,11 +1,21 @@
|
|||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
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',
|
||||
'goto_page' => 'Go to page',
|
||||
'form' => [
|
||||
'title' => 'Title',
|
||||
'name' => 'Name',
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'createSuccess' => 'User created successfully! The new user will be prompted with a password reset during his first login attempt.',
|
||||
'forcePassResetSuccess' => 'The user will be prompted with a password reset during his next login attempt.',
|
||||
'banSuccess' => 'User has been banned.',
|
||||
'unbanSuccess' => 'User has been unbanned.',
|
||||
'forcePassReset' => 'Force pass reset',
|
||||
'ban' => 'Ban',
|
||||
'unban' => 'Unban',
|
||||
'delete' => 'Delete',
|
||||
'create' => 'Create a user',
|
||||
'form' => [
|
||||
'email' => 'Email',
|
||||
'username' => 'Username',
|
||||
'password' => 'Password',
|
||||
'new_password' => 'New Password',
|
||||
'repeat_password' => 'Repeat password',
|
||||
'repeat_new_password' => 'Repeat new password',
|
||||
'submit_create' => 'Create user',
|
||||
'submit_edit' => 'Save',
|
||||
]
|
||||
];
|
|
@ -61,11 +61,30 @@ class EpisodeModel extends Model
|
|||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
$cache = \Config\Services::cache();
|
||||
|
||||
// delete cache for rss feed, podcast and episode pages
|
||||
$cache->delete(md5($episode->podcast->feed_url));
|
||||
$cache->delete(md5($episode->podcast->link));
|
||||
$cache->delete(md5($episode->link));
|
||||
cache()->delete(md5($episode->podcast->feed_url));
|
||||
cache()->delete(md5($episode->podcast->link));
|
||||
cache()->delete(md5($episode->link));
|
||||
|
||||
// delete model requests cache
|
||||
cache()->delete("{$episode->podcast_id}_episodes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all episodes for a podcast
|
||||
*
|
||||
* @param int $podcastId
|
||||
*
|
||||
* @return \App\Entities\Episode[]
|
||||
*/
|
||||
public function getPodcastEpisodes(int $podcastId): array
|
||||
{
|
||||
if (!($found = cache("{$podcastId}_episodes"))) {
|
||||
$found = $this->where('podcast_id', $podcastId)->findAll();
|
||||
|
||||
cache()->save("{$podcastId}_episodes", $found, 300);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,11 +49,9 @@ class PodcastModel extends Model
|
|||
is_array($data['id']) ? $data['id'][0] : $data['id']
|
||||
);
|
||||
|
||||
$cache = \Config\Services::cache();
|
||||
|
||||
// delete cache for rss feed and podcast pages
|
||||
$cache->delete(md5($podcast->feed_url));
|
||||
$cache->delete(md5($podcast->link));
|
||||
cache()->delete(md5($podcast->feed_url));
|
||||
cache()->delete(md5($podcast->link));
|
||||
// TODO: clear cache for every podcast's episode page?
|
||||
// foreach ($podcast->episodes as $episode) {
|
||||
// $cache->delete(md5($episode->link));
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?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;
|
||||
}
|
|
@ -14,17 +14,12 @@
|
|||
<header class="border-b">
|
||||
<div class="container flex items-center justify-between px-2 py-4 mx-auto">
|
||||
<a href="<?= route_to('home') ?>" class="text-2xl">Castopod</a>
|
||||
<nav>
|
||||
<a class="px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
||||
'podcast_create'
|
||||
) ?>">New podcast</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container flex-1 px-4 py-10 mx-auto">
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
<footer class="container px-2 py-4 mx-auto text-sm text-right border-t">
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/">Podlibre</a> initiative.
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.
|
||||
</footer>
|
||||
</body>
|
||||
</body>
|
|
@ -0,0 +1,20 @@
|
|||
<?php if (session()->has('message')): ?>
|
||||
<div class="px-4 py-2 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">
|
||||
<?= 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">
|
||||
<?php foreach (session('errors') as $error): ?>
|
||||
<li><?= $error ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif;
|
||||
?>
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Castopod</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" />
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
|
||||
<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>
|
||||
<span class="mr-2">Welcome, <?= user()->username ?></span>
|
||||
<a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to(
|
||||
'logout'
|
||||
) ?>">Logout</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<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>
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
</div>
|
||||
<footer class="container px-2 py-4 mx-auto text-sm text-right border-t">
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.
|
||||
</footer>
|
||||
</body>
|
|
@ -0,0 +1,54 @@
|
|||
<aside class="w-64 px-4 py-6">
|
||||
<nav>
|
||||
<a class="block px-2 py-1 mb-4 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
|
||||
'admin'
|
||||
) ?>">
|
||||
Dashboard
|
||||
</a>
|
||||
<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(
|
||||
'podcast_list'
|
||||
) ?>">All 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_create'
|
||||
) ?>">New podcast</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Users</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(
|
||||
'user_list'
|
||||
) ?>">All Users</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(
|
||||
'user_create'
|
||||
) ?>">New user</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">My Account</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(
|
||||
'myAccount'
|
||||
) ?>">Account info</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(
|
||||
'myAccount_change-password'
|
||||
) ?>">Change my password</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
|
@ -0,0 +1,8 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<h1 class="text-2xl">Welcome to the admin dashboard!</h1>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<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): ?>
|
||||
<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
|
||||
) ?>">
|
||||
<h3 class="text-xl font-semibold">
|
||||
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
|
||||
<span class="text-base font-bold text-gray-600">#<?= $episode->number ?></span>
|
||||
</h3>
|
||||
<p><?= $episode->description ?></p>
|
||||
</a>
|
||||
<audio controls class="mt-auto" preload="none">
|
||||
<source src="<?= $episode->enclosure_url ?>" 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
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-gray-700 hover:bg-gray-800"><?= lang(
|
||||
'Episode.goto_page'
|
||||
) ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode_delete',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>" 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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -0,0 +1,31 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<h1 class="mb-6 text-xl"><?= lang('MyAccount.changePassword') ?></h1>
|
||||
|
||||
<form action="<?= route_to(
|
||||
'myAccount_changePassword'
|
||||
) ?>" method="post" class="flex flex-col max-w-lg">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input type="hidden" name="email" value="<?= user()->email ?>">
|
||||
|
||||
<label for="password"><?= lang('User.form.password') ?></label>
|
||||
<input type="password" name="password" class="mb-4 form-input" id="password" autocomplete="off">
|
||||
|
||||
<label for="new_password"><?= lang('User.form.new_password') ?></label>
|
||||
<input type="password" name="new_password" class="mb-4 form-input" id="new_password" autocomplete="off">
|
||||
|
||||
<label for="pass_confirm"><?= lang(
|
||||
'User.form.repeat_new_password'
|
||||
) ?></label>
|
||||
<input type="password" name="new_pass_confirm" class="mb-6 form-input" id="new_pass_confirm" autocomplete="off">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('User.form.submit_edit') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -0,0 +1,31 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Email
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<?= user()->email ?>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Username
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<?= user()->username ?>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium leading-5 text-gray-500">
|
||||
Permissions
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
[<?= implode(', ', user()->permissions) ?>]
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -1,4 +1,4 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<h1 class="mb-2 text-xl"><?= lang('Podcast.all_podcasts') ?> (<?= count(
|
||||
$all_podcasts
|
||||
) ?>)</h1>
|
||||
<div class="flex flex-wrap">
|
||||
<?php if ($all_podcasts): ?>
|
||||
<?php foreach ($all_podcasts as $podcast): ?>
|
||||
<article class="w-48 h-full p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
|
||||
<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
|
||||
) ?>" 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
|
||||
) ?>"><?= 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
|
||||
) ?>"><?= lang('Podcast.see_episodes') ?></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
|
||||
) ?>"><?= 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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -0,0 +1,38 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $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">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<label for="email"><?= lang('User.form.email') ?></label>
|
||||
<input type="email" class="mb-4 form-input" name="email" id="email" value="<?= old(
|
||||
'email'
|
||||
) ?>">
|
||||
|
||||
<label for="username"><?= lang('User.form.username') ?></label>
|
||||
<input type="text" class="mb-4 form-input" name="username" id="username" value="<?= old(
|
||||
'username'
|
||||
) ?>">
|
||||
|
||||
<label for="password"><?= lang('User.form.password') ?></label>
|
||||
<input type="password" name="password" class="mb-4 form-input" id="password" autocomplete="off">
|
||||
|
||||
<label for="pass_confirm"><?= lang('User.form.repeat_password') ?></label>
|
||||
<input type="password" name="pass_confirm" class="mb-6 form-input" id="pass_confirm" autocomplete="off">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('User.form.submit_create') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -0,0 +1,61 @@
|
|||
<?= $this->extend('admin/_layout') ?>
|
||||
|
||||
<?= $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>
|
||||
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Castopod Auth</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" />
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center justify-center min-h-screen mx-auto bg-gray-100">
|
||||
<header class="mb-4">
|
||||
<a href="<?= route_to('home') ?>" class="text-2xl"><?= $this->renderSection(
|
||||
'title'
|
||||
) ?></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>
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
<footer class="flex flex-col text-sm">
|
||||
<?= $this->renderSection('footer') ?>
|
||||
<p class="py-4 border-t">
|
||||
Powered by <a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>, a <a class="underline hover:no-underline" href="https://podlibre.org/" target="_blank" rel="noreferrer noopener">Podlibre</a> initiative.
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
|
@ -0,0 +1,29 @@
|
|||
<?= $this->extend($config->viewLayout) ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Auth.resetYourPassword') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form action="<?= route_to(
|
||||
'change-password'
|
||||
) ?>" method="post" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<input type="hidden" name="token" value="<?= $token ?>">
|
||||
<input type="hidden" name="email" value="<?= $email ?>">
|
||||
|
||||
<label for="password"><?= lang('Auth.newPassword') ?></label>
|
||||
<input type="password" class="mb-4 form-input" name="password">
|
||||
|
||||
<label for="pass_confirm"><?= lang('Auth.newPasswordRepeat') ?></label>
|
||||
<input type="password" class="mb-6 form-input" name="pass_confirm">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('Auth.resetPassword') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -0,0 +1,11 @@
|
|||
<p>This is activation email for your account on <?= base_url() ?>.</p>
|
||||
|
||||
<p>To activate your account use this URL.</p>
|
||||
|
||||
<p><a href="<?= base_url('activate-account') .
|
||||
'?token=' .
|
||||
$hash ?>">Activate account</a>.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>If you did not registered on this website, you can safely ignore this email.</p>
|
|
@ -0,0 +1,13 @@
|
|||
<p>Someone requested a password reset at this email address for <?= base_url() ?>.</p>
|
||||
|
||||
<p>To reset the password use this code or URL and follow the instructions.</p>
|
||||
|
||||
<p>Your Code: <?= $hash ?></p>
|
||||
|
||||
<p>Visit the <a href="<?= base_url('reset-password') .
|
||||
'?token=' .
|
||||
$hash ?>">Reset Form</a>.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>If you did not request a password reset, you can safely ignore this email.</p>
|
|
@ -0,0 +1,25 @@
|
|||
<?= $this->extend($config->viewLayout) ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Auth.forgotPassword') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<p class="mb-4"><?= lang('Auth.enterEmailForInstructions') ?></p>
|
||||
|
||||
<form action="<?= route_to('forgot') ?>" method="post" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<label for="email"><?= lang('Auth.emailAddress') ?></label>
|
||||
<input type="email" class="mb-6 form-input" name="email" placeholder="<?= lang(
|
||||
'Auth.email'
|
||||
) ?>">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('Auth.sendInstructions') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -0,0 +1,44 @@
|
|||
<?= $this->extend($config->viewLayout) ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Auth.loginTitle') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form action="<?= route_to('login') ?>" method="post" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<label for="login"><?= lang('Auth.emailOrUsername') ?></label>
|
||||
<input type="text" name="login" class="mb-4 form-input" placeholder="<?= lang(
|
||||
'Auth.emailOrUsername'
|
||||
) ?>">
|
||||
|
||||
<label for="password"><?= lang('Auth.password') ?></label>
|
||||
<input type="password" name="password" class="mb-6 form-input" placeholder="<?= lang(
|
||||
'Auth.password'
|
||||
) ?>">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('Auth.loginAction') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('footer') ?>
|
||||
|
||||
<div class="flex flex-col items-center py-4 text-sm text-center">
|
||||
<?php if ($config->allowRegistration): ?>
|
||||
<a class="underline hover:no-underline" href="<?= route_to(
|
||||
'register'
|
||||
) ?>"><?= lang('Auth.needAnAccount') ?></a>
|
||||
<?php endif; ?>
|
||||
<a class="underline hover:no-underline" href="<?= route_to(
|
||||
'forgot'
|
||||
) ?>"><?= lang('Auth.forgotYourPassword') ?></a>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -0,0 +1,54 @@
|
|||
<?= $this->extend($config->viewLayout) ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Auth.register') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form action="<?= route_to('register') ?>" method="post" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<label for="email"><?= lang('Auth.email') ?></label>
|
||||
<input type="email" class="mb-4 form-input" name="email" aria-describedby="emailHelp" placeholder="<?= lang(
|
||||
'Auth.email'
|
||||
) ?>" value="<?= old('email') ?>">
|
||||
<small id="emailHelp" class="mb-4">
|
||||
<?= lang('Auth.weNeverShare') ?>
|
||||
</small>
|
||||
|
||||
<label for="username"><?= lang('Auth.username') ?></label>
|
||||
<input type="text" class="mb-4 form-input" name="username" placeholder="<?= lang(
|
||||
'Auth.username'
|
||||
) ?>" value="<?= old('username') ?>">
|
||||
|
||||
<label for="password"><?= lang('Auth.password') ?></label>
|
||||
<input type="password" name="password" class="mb-4 form-input" placeholder="<?= lang(
|
||||
'Auth.password'
|
||||
) ?>" autocomplete="off">
|
||||
|
||||
<label for="pass_confirm"><?= lang('Auth.repeatPassword') ?></label>
|
||||
<input type="password" name="pass_confirm" class="mb-6 form-input" placeholder="<?= lang(
|
||||
'Auth.repeatPassword'
|
||||
) ?>" autocomplete="off">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('Auth.register') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('footer') ?>
|
||||
|
||||
<p class="py-4 text-sm text-center">
|
||||
<?= lang(
|
||||
'Auth.alreadyRegistered'
|
||||
) ?> <a class="underline hover:no-underline" href="<?= route_to(
|
||||
'login'
|
||||
) ?>"><?= lang('Auth.signIn') ?></a>
|
||||
</p>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -0,0 +1,38 @@
|
|||
<?= $this->extend($config->viewLayout) ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Auth.resetYourPassword') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<p class="mb-4"><?= lang('Auth.enterCodeEmailPassword') ?></p>
|
||||
|
||||
<form action="<?= route_to(
|
||||
'reset-password'
|
||||
) ?>" method="post" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<label for="token"><?= lang('Auth.token') ?></label>
|
||||
<input type="text" class="mb-4 form-input" name="token" placeholder="<?= lang(
|
||||
'Auth.token'
|
||||
) ?>" value="<?= old('token', $token ?? '') ?>">
|
||||
|
||||
<label for="email"><?= lang('Auth.email') ?></label>
|
||||
<input type="email" class="mb-4 form-input" name="email" placeholder="<?= lang(
|
||||
'Auth.email'
|
||||
) ?>" value="<?= old('email') ?>">
|
||||
|
||||
<label for="password"><?= lang('Auth.newPassword') ?></label>
|
||||
<input type="password" class="mb-4 form-input" name="password">
|
||||
|
||||
<label for="pass_confirm"><?= lang('Auth.newPasswordRepeat') ?></label>
|
||||
<input type="password" class="mb-6 form-input" name="pass_confirm">
|
||||
|
||||
<button type="submit" class="px-4 py-2 ml-auto border">
|
||||
<?= lang('Auth.resetPassword') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -1,9 +1,9 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<a class="underline hover:no-underline" href="<?= route_to(
|
||||
'podcast_view',
|
||||
'podcast',
|
||||
$podcast->name
|
||||
) ?>">< <?= lang('Episode.back_to_podcast') ?></a>
|
||||
<h1 class="text-2xl font-semibold"><?= $episode->title ?></h1>
|
||||
|
@ -13,18 +13,5 @@
|
|||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
|
||||
<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
|
||||
) ?>"><?= lang('Episode.edit') ?></a>
|
||||
<a href="<?= route_to(
|
||||
'episode_delete',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
|
||||
'Episode.delete'
|
||||
) ?></a>
|
||||
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->endSection()
|
||||
?>
|
|
@ -1,4 +1,4 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
<section class="flex flex-wrap">
|
||||
<?php if ($podcasts): ?>
|
||||
<?php foreach ($podcasts as $podcast): ?>
|
||||
<a href="<?= route_to('podcast_view', $podcast->name) ?>">
|
||||
<a href="<?= route_to('podcast', $podcast->name) ?>">
|
||||
<article class="w-48 h-full p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
|
||||
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
|
||||
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
|
||||
|
@ -21,4 +21,5 @@
|
|||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->endSection()
|
||||
?>
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
<?= $this->extend('layouts/default') ?>
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<header class="py-4 border-b">
|
||||
<h1 class="text-2xl"><?= $podcast->title ?></h1>
|
||||
<img src="<?= $podcast->image_url ?>" alt="Podcast cover" class="object-cover w-40 h-40 mb-6" />
|
||||
<a class="inline-flex px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
|
||||
'episode_create',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.new_episode') ?></a>
|
||||
<a class="inline-flex px-4 py-2 bg-orange-500 hover:bg-orange-600" href="<?= route_to(
|
||||
'podcast_feed',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.feed') ?></a>
|
||||
<a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
|
||||
'podcast_edit',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.edit') ?></a>
|
||||
<a class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
|
||||
'podcast_delete',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.delete') ?></a>
|
||||
<a class="inline-flex px-4 py-2 bg-orange-500 hover:bg-orange-600" href="<?= route_to(
|
||||
'podcast_feed',
|
||||
$podcast->name
|
||||
) ?>"><?= lang('Podcast.feed') ?></a>
|
||||
</header>
|
||||
|
||||
<section class="flex flex-col py-4">
|
||||
|
@ -31,11 +19,7 @@
|
|||
<article class="flex w-full max-w-lg p-4 mb-4 border shadow">
|
||||
<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_view',
|
||||
$podcast->name,
|
||||
$episode->slug
|
||||
) ?>">
|
||||
<a href="<?= $episode->link ?>">
|
||||
<h3 class="text-xl font-semibold">
|
||||
<span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
|
||||
<span class="text-base font-bold text-gray-600">#<?= $episode->number ?></span>
|
|
@ -9,7 +9,8 @@
|
|||
"codeigniter4/framework": "^4",
|
||||
"james-heinrich/getid3": "~2.0.0-dev",
|
||||
"whichbrowser/parser": "^2.0",
|
||||
"geoip2/geoip2": "~2.0"
|
||||
"geoip2/geoip2": "~2.0",
|
||||
"myth/auth": "1.0-beta.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
|
|
|
@ -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": "8db0ba517a2c2b9718293a386c05c746",
|
||||
"content-hash": "a03d5be6665057254fa301cada96586e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
|
@ -533,6 +533,56 @@
|
|||
"homepage": "https://github.com/maxmind/web-service-common-php",
|
||||
"time": "2020-05-06T14:07:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myth/auth",
|
||||
"version": "1.0-beta.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lonnieezell/myth-auth.git",
|
||||
"reference": "b110088785ba22a82264e1df444621f3e1618f95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/b110088785ba22a82264e1df444621f3e1618f95",
|
||||
"reference": "b110088785ba22a82264e1df444621f3e1618f95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeigniter4/codeigniter4": "dev-develop",
|
||||
"fzaninotto/faker": "^1.9@dev",
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Myth\\Auth\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lonnie Ezell",
|
||||
"email": "lonnieje@gmail.com",
|
||||
"homepage": "http://newmythmedia.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Flexible authentication/authorization system for CodeIgniter 4.",
|
||||
"homepage": "https://github.com/lonnieezell/myth-auth",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"authorization",
|
||||
"codeigniter"
|
||||
],
|
||||
"time": "2019-12-12T05:12:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "1.0.1",
|
||||
|
@ -805,20 +855,20 @@
|
|||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.9.5",
|
||||
"version": "1.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
|
||||
"reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
|
||||
"reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
|
||||
"reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1"
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"replace": {
|
||||
"myclabs/deep-copy": "self.version"
|
||||
|
@ -849,7 +899,13 @@
|
|||
"object",
|
||||
"object graph"
|
||||
],
|
||||
"time": "2020-01-17T21:11:47+00:00"
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-06-29T13:22:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phar-io/manifest",
|
||||
|
@ -955,25 +1011,25 @@
|
|||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
|
||||
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
"dev-2.x": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -1000,7 +1056,7 @@
|
|||
"reflection",
|
||||
"static analysis"
|
||||
],
|
||||
"time": "2020-04-27T09:25:28+00:00"
|
||||
"time": "2020-06-27T09:03:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
|
@ -1057,25 +1113,24 @@
|
|||
},
|
||||
{
|
||||
"name": "phpdocumentor/type-resolver",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/TypeResolver.git",
|
||||
"reference": "30441f2752e493c639526b215ed81d54f369d693"
|
||||
"reference": "e878a14a65245fbe78f8080eba03b47c3b705651"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30441f2752e493c639526b215ed81d54f369d693",
|
||||
"reference": "30441f2752e493c639526b215ed81d54f369d693",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651",
|
||||
"reference": "e878a14a65245fbe78f8080eba03b47c3b705651",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2",
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpdocumentor/reflection-common": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-tokenizer": "^7.2",
|
||||
"mockery/mockery": "~1"
|
||||
"ext-tokenizer": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
|
@ -1099,7 +1154,7 @@
|
|||
}
|
||||
],
|
||||
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
|
||||
"time": "2020-06-19T20:22:09+00:00"
|
||||
"time": "2020-06-27T10:12:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
|
@ -2293,7 +2348,8 @@
|
|||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"james-heinrich/getid3": 20
|
||||
"james-heinrich/getid3": 20,
|
||||
"myth/auth": 10
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
|
|
|
@ -98,7 +98,7 @@ Build the database with the migrate command:
|
|||
|
||||
```bash
|
||||
# loads the database schema during first migration
|
||||
docker-compose run --rm app php spark migrate
|
||||
docker-compose run --rm app php spark migrate -all
|
||||
```
|
||||
|
||||
Populate the database with the required data:
|
||||
|
|
Loading…
Reference in New Issue