From c63a077618c61b4cde7f25ffc650a4b0e1495f44 Mon Sep 17 00:00:00 2001 From: Yassine Doghri <yassine@doghri.fr> Date: Fri, 10 Jul 2020 12:20:25 +0000 Subject: [PATCH] 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 --- DEPENDENCIES.md | 1 + app/Config/App.php | 16 ++ app/Config/Auth.php | 42 ++++ app/Config/Filters.php | 12 ++ app/Config/Routes.php | 150 +++++++++++++-- app/Config/Toolbar.php | 1 + app/Config/Validation.php | 1 + app/Controllers/Admin/BaseController.php | 48 +++++ app/Controllers/Admin/Episode.php | 168 ++++++++++++++++ app/Controllers/Admin/Home.php | 16 ++ app/Controllers/Admin/Myaccount.php | 73 +++++++ app/Controllers/Admin/Podcast.php | 181 ++++++++++++++++++ app/Controllers/Admin/User.php | 142 ++++++++++++++ app/Controllers/Analytics.php | 4 +- app/Controllers/Auth.php | 98 ++++++++++ app/Controllers/BaseController.php | 4 +- app/Controllers/Episode.php | 137 +------------ app/Controllers/Feed.php | 5 + app/Controllers/Home.php | 4 +- app/Controllers/Podcast.php | 138 +------------ app/Controllers/UnknownUserAgents.php | 9 +- .../2020-07-03-191500_add_users_podcasts.php | 48 +++++ app/Database/Seeds/UserSeeder.php | 30 +++ app/Entities/Episode.php | 2 +- app/Entities/Podcast.php | 23 ++- app/Entities/UserPodcast.php | 18 ++ app/Helpers/analytics_helper.php | 1 - app/Helpers/media_helper.php | 2 +- app/Language/en/Episode.php | 8 + app/Language/en/Home.php | 5 + app/Language/en/MyAccount.php | 11 ++ app/Language/en/Podcast.php | 10 + app/Language/en/User.php | 28 +++ app/Models/EpisodeModel.php | 29 ++- app/Models/PodcastModel.php | 6 +- app/Models/UserPodcastModel.php | 23 +++ .../{layouts/default.php => _layout.php} | 9 +- app/Views/_message_block.php | 20 ++ app/Views/admin/_layout.php | 37 ++++ app/Views/admin/_sidenav.php | 54 ++++++ app/Views/admin/dashboard.php | 8 + app/Views/{ => admin}/episode/create.php | 2 +- app/Views/{ => admin}/episode/edit.php | 2 +- app/Views/admin/episode/list.php | 65 +++++++ .../admin/my_account/change_password.php | 31 +++ app/Views/admin/my_account/view.php | 31 +++ app/Views/{ => admin}/podcast/create.php | 2 +- app/Views/{ => admin}/podcast/edit.php | 2 +- app/Views/admin/podcast/list.php | 49 +++++ app/Views/admin/user/create.php | 38 ++++ app/Views/admin/user/list.php | 61 ++++++ app/Views/auth/_layout.php | 31 +++ app/Views/auth/change_password.php | 29 +++ app/Views/auth/emails/activation.php | 11 ++ app/Views/auth/emails/forgot.php | 13 ++ app/Views/auth/forgot.php | 25 +++ app/Views/auth/login.php | 44 +++++ app/Views/auth/register.php | 54 ++++++ app/Views/auth/reset.php | 38 ++++ app/Views/{episode/view.php => episode.php} | 21 +- app/Views/home.php | 7 +- app/Views/{podcast/view.php => podcast.php} | 28 +-- composer.json | 3 +- composer.lock | 102 +++++++--- docs/setup-development.md | 2 +- 65 files changed, 1921 insertions(+), 392 deletions(-) create mode 100644 app/Config/Auth.php create mode 100644 app/Controllers/Admin/BaseController.php create mode 100644 app/Controllers/Admin/Episode.php create mode 100644 app/Controllers/Admin/Home.php create mode 100644 app/Controllers/Admin/Myaccount.php create mode 100644 app/Controllers/Admin/Podcast.php create mode 100644 app/Controllers/Admin/User.php create mode 100644 app/Controllers/Auth.php create mode 100644 app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php create mode 100644 app/Database/Seeds/UserSeeder.php create mode 100644 app/Entities/UserPodcast.php create mode 100644 app/Language/en/MyAccount.php create mode 100644 app/Language/en/User.php create mode 100644 app/Models/UserPodcastModel.php rename app/Views/{layouts/default.php => _layout.php} (77%) create mode 100644 app/Views/_message_block.php create mode 100644 app/Views/admin/_layout.php create mode 100644 app/Views/admin/_sidenav.php create mode 100644 app/Views/admin/dashboard.php rename app/Views/{ => admin}/episode/create.php (98%) rename app/Views/{ => admin}/episode/edit.php (99%) create mode 100644 app/Views/admin/episode/list.php create mode 100644 app/Views/admin/my_account/change_password.php create mode 100644 app/Views/admin/my_account/view.php rename app/Views/{ => admin}/podcast/create.php (99%) rename app/Views/{ => admin}/podcast/edit.php (99%) create mode 100644 app/Views/admin/podcast/list.php create mode 100644 app/Views/admin/user/create.php create mode 100644 app/Views/admin/user/list.php create mode 100644 app/Views/auth/_layout.php create mode 100644 app/Views/auth/change_password.php create mode 100644 app/Views/auth/emails/activation.php create mode 100644 app/Views/auth/emails/forgot.php create mode 100644 app/Views/auth/forgot.php create mode 100644 app/Views/auth/login.php create mode 100644 app/Views/auth/register.php create mode 100644 app/Views/auth/reset.php rename app/Views/{episode/view.php => episode.php} (51%) rename app/Views/{podcast/view.php => podcast.php} (63%) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index b859ea4540..77fc51eb8d 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -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)) diff --git a/app/Config/App.php b/app/Config/App.php index a12599ff0e..0f03019e94 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -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'; } diff --git a/app/Config/Auth.php b/app/Config/Auth.php new file mode 100644 index 0000000000..6c5b78f5a2 --- /dev/null +++ b/app/Config/Auth.php @@ -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; +} diff --git a/app/Config/Filters.php b/app/Config/Filters.php index f76ceb2016..fef58ff5f2 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -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 . '*']], + ]; + } } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 5db584e1c8..494a3f62c0 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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 diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 639d431ab2..14d3e7bd3b 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -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, ]; /* diff --git a/app/Config/Validation.php b/app/Config/Validation.php index ba4ac7cd4f..d93c623fc2 100644 --- a/app/Config/Validation.php +++ b/app/Config/Validation.php @@ -17,6 +17,7 @@ class Validation \CodeIgniter\Validation\FormatRules::class, \CodeIgniter\Validation\FileRules::class, \CodeIgniter\Validation\CreditCardRules::class, + \Myth\Auth\Authentication\Passwords\ValidationRules::class, ]; /** diff --git a/app/Controllers/Admin/BaseController.php b/app/Controllers/Admin/BaseController.php new file mode 100644 index 0000000000..a10692c9e7 --- /dev/null +++ b/app/Controllers/Admin/BaseController.php @@ -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(); + } +} diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php new file mode 100644 index 0000000000..105aed974e --- /dev/null +++ b/app/Controllers/Admin/Episode.php @@ -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]); + } +} diff --git a/app/Controllers/Admin/Home.php b/app/Controllers/Admin/Home.php new file mode 100644 index 0000000000..6e3b80aa01 --- /dev/null +++ b/app/Controllers/Admin/Home.php @@ -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'); + } +} diff --git a/app/Controllers/Admin/Myaccount.php b/app/Controllers/Admin/Myaccount.php new file mode 100644 index 0000000000..5cd23535dd --- /dev/null +++ b/app/Controllers/Admin/Myaccount.php @@ -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')); + } +} diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php new file mode 100644 index 0000000000..502f15322a --- /dev/null +++ b/app/Controllers/Admin/Podcast.php @@ -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'); + } +} diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php new file mode 100644 index 0000000000..4faffc2363 --- /dev/null +++ b/app/Controllers/Admin/User.php @@ -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')); + } +} diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index f001a45a56..9a2621c24c 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -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 diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php new file mode 100644 index 0000000000..f91900654f --- /dev/null +++ b/app/Controllers/Auth.php @@ -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')); + } +} diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 5e3a8c0209..58cd127630 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -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 diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index fc14e8957c..4721a6582c 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -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); } } diff --git a/app/Controllers/Feed.php b/app/Controllers/Feed.php index e76eb19924..48064d0a6e 100644 --- a/app/Controllers/Feed.php +++ b/app/Controllers/Feed.php @@ -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; diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index f87459b6fb..0f9ca24fb2 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -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 diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 081f7b9faf..70db9d6059 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -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); } } diff --git a/app/Controllers/UnknownUserAgents.php b/app/Controllers/UnknownUserAgents.php index 82e1a2985b..3eb284712a 100644 --- a/app/Controllers/UnknownUserAgents.php +++ b/app/Controllers/UnknownUserAgents.php @@ -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 diff --git a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php new file mode 100644 index 0000000000..16381819dc --- /dev/null +++ b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php @@ -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'); + } +} diff --git a/app/Database/Seeds/UserSeeder.php b/app/Database/Seeds/UserSeeder.php new file mode 100644 index 0000000000..826f1332a1 --- /dev/null +++ b/app/Database/Seeds/UserSeeder.php @@ -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); + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 243bf3bd00..2087271637 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -135,7 +135,7 @@ class Episode extends Entity { return base_url( route_to( - 'episode_view', + 'episode', $this->getPodcast()->name, $this->attributes['slug'] ) diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 2a3407d56f..386be65c3f 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -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.' + ); + } + + if (empty($this->permissions)) { + $this->episodes = (new EpisodeModel())->getPodcastEpisodes( + $this->id + ); + } - return $episode_model - ->where('podcast_id', $this->attributes['id']) - ->findAll(); + return $this->episodes; } } diff --git a/app/Entities/UserPodcast.php b/app/Entities/UserPodcast.php new file mode 100644 index 0000000000..d951557dc9 --- /dev/null +++ b/app/Entities/UserPodcast.php @@ -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', + ]; +} diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index 73ebce65fc..90282c2ada 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -13,7 +13,6 @@ function set_user_session_country() { $session = \Config\Services::session(); $session->start(); - $db = \Config\Database::connect(); $country = 'N/A'; diff --git a/app/Helpers/media_helper.php b/app/Helpers/media_helper.php index 09edaf196f..3a3628e6bc 100644 --- a/app/Helpers/media_helper.php +++ b/app/Helpers/media_helper.php @@ -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 * diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 8915c8de9b..22ff6a5349 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -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', diff --git a/app/Language/en/Home.php b/app/Language/en/Home.php index 3cb0366cb8..435307ef9d 100644 --- a/app/Language/en/Home.php +++ b/app/Language/en/Home.php @@ -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', diff --git a/app/Language/en/MyAccount.php b/app/Language/en/MyAccount.php new file mode 100644 index 0000000000..c1935bcc33 --- /dev/null +++ b/app/Language/en/MyAccount.php @@ -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' +]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 776420a28e..767d54e089 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -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', diff --git a/app/Language/en/User.php b/app/Language/en/User.php new file mode 100644 index 0000000000..fcf9134c64 --- /dev/null +++ b/app/Language/en/User.php @@ -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', + ] +]; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index bd29dbf608..ed2ea4e9f8 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -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; } } diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index fba25f5ce7..09712b6a4e 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -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)); diff --git a/app/Models/UserPodcastModel.php b/app/Models/UserPodcastModel.php new file mode 100644 index 0000000000..32d36b0512 --- /dev/null +++ b/app/Models/UserPodcastModel.php @@ -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; +} diff --git a/app/Views/layouts/default.php b/app/Views/_layout.php similarity index 77% rename from app/Views/layouts/default.php rename to app/Views/_layout.php index 6f23967094..92d581bad9 100644 --- a/app/Views/layouts/default.php +++ b/app/Views/_layout.php @@ -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> \ No newline at end of file +</body> diff --git a/app/Views/_message_block.php b/app/Views/_message_block.php new file mode 100644 index 0000000000..466b5780b9 --- /dev/null +++ b/app/Views/_message_block.php @@ -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; +?> diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php new file mode 100644 index 0000000000..a96b0a7cf4 --- /dev/null +++ b/app/Views/admin/_layout.php @@ -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> diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php new file mode 100644 index 0000000000..f68ae5bab7 --- /dev/null +++ b/app/Views/admin/_sidenav.php @@ -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> diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php new file mode 100644 index 0000000000..910003fae6 --- /dev/null +++ b/app/Views/admin/dashboard.php @@ -0,0 +1,8 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('content') ?> + +<h1 class="text-2xl">Welcome to the admin dashboard!</h1> + +<?= $this->endSection() ?> + diff --git a/app/Views/episode/create.php b/app/Views/admin/episode/create.php similarity index 98% rename from app/Views/episode/create.php rename to app/Views/admin/episode/create.php index 606477083b..af1e9d4069 100644 --- a/app/Views/episode/create.php +++ b/app/Views/admin/episode/create.php @@ -1,4 +1,4 @@ -<?= $this->extend('layouts/default') ?> +<?= $this->extend('admin/_layout') ?> <?= $this->section('content') ?> diff --git a/app/Views/episode/edit.php b/app/Views/admin/episode/edit.php similarity index 99% rename from app/Views/episode/edit.php rename to app/Views/admin/episode/edit.php index 4ef949f6af..51b91d984d 100644 --- a/app/Views/episode/edit.php +++ b/app/Views/admin/episode/edit.php @@ -1,4 +1,4 @@ -<?= $this->extend('layouts/default') ?> +<?= $this->extend('admin/_layout') ?> <?= $this->section('content') ?> diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php new file mode 100644 index 0000000000..81d4c1f9a3 --- /dev/null +++ b/app/Views/admin/episode/list.php @@ -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() +?> diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php new file mode 100644 index 0000000000..28cc35eaaa --- /dev/null +++ b/app/Views/admin/my_account/change_password.php @@ -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() +?> diff --git a/app/Views/admin/my_account/view.php b/app/Views/admin/my_account/view.php new file mode 100644 index 0000000000..03deb336a2 --- /dev/null +++ b/app/Views/admin/my_account/view.php @@ -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() +?> diff --git a/app/Views/podcast/create.php b/app/Views/admin/podcast/create.php similarity index 99% rename from app/Views/podcast/create.php rename to app/Views/admin/podcast/create.php index 44ab5ed784..c9eed74eb1 100644 --- a/app/Views/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -1,4 +1,4 @@ -<?= $this->extend('layouts/default') ?> +<?= $this->extend('admin/_layout') ?> <?= $this->section('content') ?> diff --git a/app/Views/podcast/edit.php b/app/Views/admin/podcast/edit.php similarity index 99% rename from app/Views/podcast/edit.php rename to app/Views/admin/podcast/edit.php index 4c83baf05d..4dcfff666b 100644 --- a/app/Views/podcast/edit.php +++ b/app/Views/admin/podcast/edit.php @@ -1,4 +1,4 @@ -<?= $this->extend('layouts/default') ?> +<?= $this->extend('admin/_layout') ?> <?= $this->section('content') ?> diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php new file mode 100644 index 0000000000..7510eeef26 --- /dev/null +++ b/app/Views/admin/podcast/list.php @@ -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() +?> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php new file mode 100644 index 0000000000..82bfe99d25 --- /dev/null +++ b/app/Views/admin/user/create.php @@ -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() +?> diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php new file mode 100644 index 0000000000..a0d121213b --- /dev/null +++ b/app/Views/admin/user/list.php @@ -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() +?> diff --git a/app/Views/auth/_layout.php b/app/Views/auth/_layout.php new file mode 100644 index 0000000000..24d4aff6b3 --- /dev/null +++ b/app/Views/auth/_layout.php @@ -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> diff --git a/app/Views/auth/change_password.php b/app/Views/auth/change_password.php new file mode 100644 index 0000000000..3ca545b6e7 --- /dev/null +++ b/app/Views/auth/change_password.php @@ -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() ?> diff --git a/app/Views/auth/emails/activation.php b/app/Views/auth/emails/activation.php new file mode 100644 index 0000000000..d76eb17fcf --- /dev/null +++ b/app/Views/auth/emails/activation.php @@ -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> \ No newline at end of file diff --git a/app/Views/auth/emails/forgot.php b/app/Views/auth/emails/forgot.php new file mode 100644 index 0000000000..f6509c83c1 --- /dev/null +++ b/app/Views/auth/emails/forgot.php @@ -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> diff --git a/app/Views/auth/forgot.php b/app/Views/auth/forgot.php new file mode 100644 index 0000000000..cef9a08805 --- /dev/null +++ b/app/Views/auth/forgot.php @@ -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() ?> diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000000..2d7fcee16d --- /dev/null +++ b/app/Views/auth/login.php @@ -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() ?> diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php new file mode 100644 index 0000000000..571aff38fc --- /dev/null +++ b/app/Views/auth/register.php @@ -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() ?> diff --git a/app/Views/auth/reset.php b/app/Views/auth/reset.php new file mode 100644 index 0000000000..d2bfbce9b4 --- /dev/null +++ b/app/Views/auth/reset.php @@ -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() ?> diff --git a/app/Views/episode/view.php b/app/Views/episode.php similarity index 51% rename from app/Views/episode/view.php rename to app/Views/episode.php index f0732d64fa..aea7ddd9cb 100644 --- a/app/Views/episode/view.php +++ b/app/Views/episode.php @@ -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() +?> diff --git a/app/Views/home.php b/app/Views/home.php index 5cb1d1846d..1cd0048d96 100644 --- a/app/Views/home.php +++ b/app/Views/home.php @@ -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() +?> diff --git a/app/Views/podcast/view.php b/app/Views/podcast.php similarity index 63% rename from app/Views/podcast/view.php rename to app/Views/podcast.php index 8dda6411b1..b4ba052aa7 100644 --- a/app/Views/podcast/view.php +++ b/app/Views/podcast.php @@ -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> diff --git a/composer.json b/composer.json index 15ed68bb90..3c0f646cd0 100644 --- a/composer.json +++ b/composer.json @@ -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.*", diff --git a/composer.lock b/composer.lock index 19b6d1e112..16d8fd9a7c 100644 --- a/composer.lock +++ b/composer.lock @@ -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, diff --git a/docs/setup-development.md b/docs/setup-development.md index a2673ae716..554f2263cf 100644 --- a/docs/setup-development.md +++ b/docs/setup-development.md @@ -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: -- GitLab