diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index b859ea45..77fc51eb 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 a12599ff..0f03019e 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 00000000..6c5b78f5 --- /dev/null +++ b/app/Config/Auth.php @@ -0,0 +1,42 @@ + '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 f76ceb20..fef58ff5 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 5db584e1..494a3f62 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 639d431a..14d3e7bd 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 ba4ac7cd..d93c623f 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 00000000..a10692c9 --- /dev/null +++ b/app/Controllers/Admin/BaseController.php @@ -0,0 +1,48 @@ +session = \Config\Services::session(); + } +} diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php new file mode 100644 index 00000000..105aed97 --- /dev/null +++ b/app/Controllers/Admin/Episode.php @@ -0,0 +1,168 @@ +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 00000000..6e3b80aa --- /dev/null +++ b/app/Controllers/Admin/Home.php @@ -0,0 +1,16 @@ + '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 00000000..502f1532 --- /dev/null +++ b/app/Controllers/Admin/Podcast.php @@ -0,0 +1,181 @@ + 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 00000000..4faffc23 --- /dev/null +++ b/app/Controllers/Admin/User.php @@ -0,0 +1,142 @@ + 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 f001a45a..9a2621c2 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -1,4 +1,4 @@ - $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 5e3a8c02..58cd1276 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -1,7 +1,5 @@ $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 e76eb199..48064d0a 100644 --- a/app/Controllers/Feed.php +++ b/app/Controllers/Feed.php @@ -1,4 +1,9 @@ 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 081f7b9f..70db9d60 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 82e1a298..3eb28471 100644 --- a/app/Controllers/UnknownUserAgents.php +++ b/app/Controllers/UnknownUserAgents.php @@ -1,4 +1,11 @@ -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 00000000..826f1332 --- /dev/null +++ b/app/Database/Seeds/UserSeeder.php @@ -0,0 +1,30 @@ + '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 243bf3bd..20872716 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 2a3407d5..386be65c 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.' + ); + } - return $episode_model - ->where('podcast_id', $this->attributes['id']) - ->findAll(); + if (empty($this->permissions)) { + $this->episodes = (new EpisodeModel())->getPodcastEpisodes( + $this->id + ); + } + + return $this->episodes; } } diff --git a/app/Entities/UserPodcast.php b/app/Entities/UserPodcast.php new file mode 100644 index 00000000..d951557d --- /dev/null +++ b/app/Entities/UserPodcast.php @@ -0,0 +1,18 @@ + 'integer', + 'podcast_id' => 'integer', + ]; +} diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index 73ebce65..90282c2a 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 09edaf19..3a3628e6 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 8915c8de..22ff6a53 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 3cb0366c..435307ef 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 00000000..c1935bcc --- /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 776420a2..767d54e0 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 00000000..fcf9134c --- /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 bd29dbf6..ed2ea4e9 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 fba25f5c..09712b6a 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 00000000..32d36b05 --- /dev/null +++ b/app/Models/UserPodcastModel.php @@ -0,0 +1,23 @@ +
\ No newline at end of file + diff --git a/app/Views/_message_block.php b/app/Views/_message_block.php new file mode 100644 index 00000000..466b5780 --- /dev/null +++ b/app/Views/_message_block.php @@ -0,0 +1,20 @@ +has('message')): ?> +
+ + +has('error')): ?> +
+ + +has('errors')): ?> +
+ diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php new file mode 100644 index 00000000..a96b0a7c --- /dev/null +++ b/app/Views/admin/_layout.php @@ -0,0 +1,37 @@ + + + +
+ +
+ + + + + + +
diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php new file mode 100644 index 00000000..f68ae5ba --- /dev/null +++ b/app/Views/admin/_sidenav.php @@ -0,0 +1,54 @@ + diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php new file mode 100644 index 00000000..910003fa --- /dev/null +++ b/app/Views/admin/dashboard.php @@ -0,0 +1,8 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
+ += $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 60647708..af1e9d40 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 4ef949f6..51b91d98 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 00000000..81d4c1f9 --- /dev/null +++ b/app/Views/admin/episode/list.php @@ -0,0 +1,65 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
= lang('Podcast.no_episode') ?>
+ = lang('Episode.create_one') ?> ++ += $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 00000000..28cc35ea --- /dev/null +++ b/app/Views/admin/my_account/change_password.php @@ -0,0 +1,31 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
+ +
+ += $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 00000000..03deb336 --- /dev/null +++ b/app/Views/admin/my_account/view.php @@ -0,0 +1,31 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
+
+
+ += $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 44ab5ed7..c9eed74e 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 4c83baf0..4dcfff66 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 00000000..7510eeef --- /dev/null +++ b/app/Views/admin/podcast/list.php @@ -0,0 +1,49 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
+
@= $podcast->name ?>
+ = lang('Podcast.edit') ?> + = lang('Podcast.see_episodes') ?> + = lang('Podcast.goto_page') ?> + = lang('Podcast.delete') ?> += lang('Podcast.no_podcast') ?>
+ = lang('Podcast.create_one') ?> ++ += $this->endSection() +?> diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php new file mode 100644 index 00000000..82bfe99d --- /dev/null +++ b/app/Views/admin/user/create.php @@ -0,0 +1,38 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
+ +
+ +
+ += $this->endSection() +?> diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php new file mode 100644 index 00000000..a0d12121 --- /dev/null +++ b/app/Views/admin/user/list.php @@ -0,0 +1,61 @@ += $this->extend('admin/_layout') ?> + += $this->section('content') ?> + +
Username | +Permissions | +Banned? | +Actions | +|
---|---|---|---|---|
= $user->username ?> | += $user->email ?> | +[= implode( + ', ', + $user->permissions + ) ?>] | += $user->isBanned() + ? 'Yes' + : 'No' ?> | ++ = lang('User.forcePassReset') ?> + + = $user->isBanned() + ? lang('User.unban') + : lang('User.ban') ?> + = lang('User.delete') ?> + | +
= lang('Podcast.no_podcast') ?>
+ = lang('Podcast.create_one') ?> ++ += $this->endSection() +?> diff --git a/app/Views/auth/_layout.php b/app/Views/auth/_layout.php new file mode 100644 index 00000000..24d4aff6 --- /dev/null +++ b/app/Views/auth/_layout.php @@ -0,0 +1,31 @@ + + + +
+ +
+ + + + + + +
diff --git a/app/Views/auth/change_password.php b/app/Views/auth/change_password.php new file mode 100644 index 00000000..3ca545b6 --- /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') ?> + +
+ += $this->endSection() ?> diff --git a/app/Views/auth/emails/activation.php b/app/Views/auth/emails/activation.php new file mode 100644 index 00000000..d76eb17f --- /dev/null +++ b/app/Views/auth/emails/activation.php @@ -0,0 +1,11 @@ +
This is activation email for your account on = base_url() ?>.
+ +
To activate your account use this URL.
+ +
+
+
+
+
If you did not registered on this website, you can safely ignore this email.
\ 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 00000000..f6509c83 --- /dev/null +++ b/app/Views/auth/emails/forgot.php @@ -0,0 +1,13 @@ +
Someone requested a password reset at this email address for = base_url() ?>.
+ +
To reset the password use this code or URL and follow the instructions.
+ +
Your Code: = $hash ?>
+ +
Visit the Reset Form.
+
+
+
+
If you did not request a password reset, you can safely ignore this email.
diff --git a/app/Views/auth/forgot.php b/app/Views/auth/forgot.php new file mode 100644 index 00000000..cef9a088 --- /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') ?> + +
= lang('Auth.enterEmailForInstructions') ?>
+ +
+ += $this->endSection() ?> diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 00000000..2d7fcee1 --- /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') ?> + +
+ += $this->endSection() ?> + + += $this->section('footer') ?> + +
+ += $this->endSection() ?> diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php new file mode 100644 index 00000000..571aff38 --- /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') ?> + +
+ += $this->endSection() ?> + + += $this->section('footer') ?> + +
+ = lang( + 'Auth.alreadyRegistered' + ) ?> = lang('Auth.signIn') ?> +
+ += $this->endSection() ?> diff --git a/app/Views/auth/reset.php b/app/Views/auth/reset.php new file mode 100644 index 00000000..d2bfbce9 --- /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') ?> + +
= lang('Auth.enterCodeEmailPassword') ?>
+ +
+ += $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 f0732d64..aea7ddd9 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') ?> < = lang('Episode.back_to_podcast') ?>
@@ -13,18 +13,5 @@
Your browser does not support the audio tag.
-= lang('Episode.edit') ?>
-= lang(
- 'Episode.delete'
-) ?>
-
-
-= $this->endSection() ?>
+= $this->endSection()
+?>
diff --git a/app/Views/home.php b/app/Views/home.php
index 5cb1d184..1cd0048d 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 @@
= $podcast->title ?>
@@ -21,4 +21,5 @@
= $podcast->title ?>
-= lang('Podcast.new_episode') ?>
-= lang('Podcast.feed') ?>
-= lang('Podcast.edit') ?>
-= lang('Podcast.delete') ?>
+ = lang('Podcast.feed') ?>
= $episode->title ?>
#= $episode->number ?>
diff --git a/composer.json b/composer.json
index 15ed68bb..3c0f646c 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 19b6d1e1..16d8fd9a 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 a2673ae7..554f2263 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: