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

feat: add user permissions and basic groups to handle authorizations

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

closes #3, #18
parent c63a0776
Loading
Loading
Loading
Loading
+74 −28
Original line number Diff line number Diff line
@@ -36,11 +36,12 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}');
// route since we don't have to scan directories.
$routes->get('/', 'Home::index', ['as' => 'home']);

// Public routes
$routes->group('@(:podcastName)', function ($routes) {
    $routes->add('/', 'Podcast/$1', ['as' => 'podcast']);
    $routes->get('/', 'Podcast/$1', ['as' => 'podcast']);

    $routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
    $routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [
    $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
    $routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [
        'as' => 'episode',
    ]);
});
@@ -51,74 +52,119 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
]);

// Show the Unknown UserAgents
$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents');
$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
$routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');

// Admin area
$routes->group(
    config('App')->adminGateway,
    ['namespace' => 'App\Controllers\Admin'],
    function ($routes) {
        $routes->add('/', 'Home', [
        $routes->get('/', 'Home', [
            'as' => 'admin',
        ]);

        $routes->add('new-podcast', 'Podcast::create', [
        $routes->get('my-podcasts', 'Podcast::myPodcasts', [
            'as' => 'my_podcasts',
        ]);
        $routes->get('podcasts', 'Podcast::list', [
            'as' => 'podcast_list',
            'filter' => 'permission:podcasts-list',
        ]);
        $routes->get('new-podcast', 'Podcast::create', [
            'as' => 'podcast_create',
            'filter' => 'permission:podcasts-create',
        ]);
        $routes->post('new-podcast', 'Podcast::attemptCreate', [
            'filter' => 'permission:podcasts-create',
        ]);
        $routes->add('podcasts', 'Podcast::list', ['as' => 'podcast_list']);

        $routes->group('podcasts/@(:podcastName)', function ($routes) {
            $routes->add('edit', 'Podcast::edit/$1', [
        // Use ids in admin area to help permission and group lookups
        $routes->group('podcasts/(:num)', function ($routes) {
            $routes->get('edit', 'Podcast::edit/$1', [
                'as' => 'podcast_edit',
            ]);
            $routes->post('edit', 'Podcast::attemptEdit/$1');
            $routes->add('delete', 'Podcast::delete/$1', [
                'as' => 'podcast_delete',
            ]);

            $routes->add('new-episode', 'Episode::create/$1', [
            // Podcast episodes
            $routes->get('episodes', 'Episode::list/$1', [
                'as' => 'episode_list',
            ]);
            $routes->get('new-episode', 'Episode::create/$1', [
                'as' => 'episode_create',
            ]);
            $routes->add('episodes', 'Episode::list/$1', [
                'as' => 'episode_list',
            $routes->post('new-episode', 'Episode::attemptCreate/$1');

            $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
                'as' => 'episode_edit',
            ]);
            $routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2');
            $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [
                'as' => 'episode_delete',
            ]);

            $routes->add(
                'episodes/(:episodeSlug)/edit',
                'Episode::edit/$1/$2',
            // Podcast contributors
            $routes->get('contributors', 'Contributor::list/$1', [
                'as' => 'contributor_list',
            ]);
            $routes->get('add-contributor', 'Contributor::add/$1', [
                'as' => 'contributor_add',
            ]);
            $routes->post('add-contributor', 'Contributor::attemptAdd/$1');
            $routes->get(
                'contributors/(:num)/edit',
                'Contributor::edit/$1/$2',
                [
                    'as' => 'episode_edit',
                    'as' => 'contributor_edit',
                ]
            );
            $routes->post(
                'contributors/(:num)/edit',
                'Contributor::attemptEdit/$1/$2'
            );
            $routes->add(
                'episodes/(:episodeSlug)/delete',
                'Episode::delete/$1/$2',
                [
                    'as' => 'episode_delete',
                ]
                'contributors/(:num)/remove',
                'Contributor::remove/$1/$2',
                ['as' => 'contributor_remove']
            );
        });

        // Users
        $routes->add('users', 'User::list', ['as' => 'user_list']);
        $routes->add('new-user', 'User::create', ['as' => 'user_create']);
        $routes->get('users', 'User::list', [
            'as' => 'user_list',
            'filter' => 'permission:users-list',
        ]);
        $routes->get('new-user', 'User::create', [
            'as' => 'user_create',
            'filter' => 'permission:users-create',
        ]);
        $routes->post('new-user', 'User::attemptCreate', [
            'filter' => 'permission:users-create',
        ]);

        $routes->add('users/@(:any)/ban', 'User::ban/$1', [
        $routes->add('users/(:num)/ban', 'User::ban/$1', [
            'as' => 'user_ban',
            'filter' => 'permission:users-manage_bans',
        ]);
        $routes->add('users/@(:any)/unban', 'User::unBan/$1', [
        $routes->add('users/(:num)/unban', 'User::unBan/$1', [
            'as' => 'user_unban',
            'filter' => 'permission:users-manage_bans',
        ]);
        $routes->add(
            'users/@(:any)/force-pass-reset',
            'users/(:num)/force-pass-reset',
            'User::forcePassReset/$1',
            [
                'as' => 'user_force_pass_reset',
                'filter' => 'permission:users-force_pass_reset',
            ]
        );

        $routes->add('users/@(:any)/delete', 'User::delete/$1', [
        $routes->add('users/(:num)/delete', 'User::delete/$1', [
            'as' => 'user_delete',
            'filter' => 'permission:users-delete',
        ]);

        // My account
+187 −0
Original line number Diff line number Diff line
<?php
/**
 * @copyright  2020 Podlibre
 * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
 * @link       https://castopod.org/
 */

namespace App\Controllers\Admin;

use App\Models\PodcastModel;
use Myth\Auth\Authorization\GroupModel;
use Myth\Auth\Config\Services;
use Myth\Auth\Models\UserModel;

class Contributor extends BaseController
{
    protected \App\Entities\Podcast $podcast;
    protected ?\Myth\Auth\Entities\User $user;

    public function _remap($method, ...$params)
    {
        if (
            !has_permission('podcasts-manage_contributors') ||
            !has_permission("podcasts:$params[0]-manage_contributors")
        ) {
            throw new \RuntimeException(lang('Auth.notEnoughPrivilege'));
        }

        $podcast_model = new PodcastModel();

        $this->podcast = $podcast_model->find($params[0]);

        if (count($params) > 1) {
            $user_model = new UserModel();
            if (
                !($this->user = $user_model
                    ->select('users.*')
                    ->join(
                        'users_podcasts',
                        'users_podcasts.user_id = users.id'
                    )
                    ->where([
                        'users.id' => $params[1],
                        'podcast_id' => $params[0],
                    ])
                    ->first())
            ) {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
        }

        return $this->$method();
    }

    public function list()
    {
        $data = [
            'podcast' => $this->podcast,
        ];

        echo view('admin/contributor/list', $data);
    }

    public function add()
    {
        $user_model = new UserModel();
        $group_model = new GroupModel();

        $roles = $group_model
            ->select('auth_groups.*')
            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
            ->findAll();

        $data = [
            'podcast' => $this->podcast,
            'users' => $user_model->findAll(),
            'roles' => $roles,
        ];

        echo view('admin/contributor/add', $data);
    }

    public function attemptAdd()
    {
        $authorize = Services::authorization();

        $user_id = (int) $this->request->getPost('user');
        $group_id = (int) $this->request->getPost('role');

        // Add user to chosen group
        $authorize->addUserToGroup($user_id, $group_id);

        (new PodcastModel())->addContributorToPodcast(
            $user_id,
            $this->podcast->id
        );

        return redirect()->route('contributor_list', [$this->podcast->id]);
    }

    public function edit()
    {
        $group_model = new GroupModel();

        $roles = $group_model
            ->select('auth_groups.*')
            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
            ->findAll();

        $user_role = $group_model
            ->select('auth_groups.*')
            ->join(
                'auth_groups_users',
                'auth_groups_users.group_id = auth_groups.id'
            )
            ->where('auth_groups_users.user_id', $this->user->id)
            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
            ->first();

        $data = [
            'podcast' => $this->podcast,
            'user' => $this->user,
            'user_role' => $user_role,
            'roles' => $roles,
        ];

        echo view('admin/contributor/edit', $data);
    }

    public function attemptEdit()
    {
        $authorize = Services::authorization();

        $group_model = new GroupModel();

        $group = $group_model
            ->select('auth_groups.*')
            ->join(
                'auth_groups_users',
                'auth_groups_users.group_id = auth_groups.id'
            )
            ->where('user_id', $this->user->id)
            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
            ->first();

        $authorize->removeUserFromGroup(
            (int) $this->user->id,
            (int) $group->id
        );

        $authorize->addUserToGroup(
            (int) $this->user->id,
            (int) $this->request->getPost('role')
        );

        return redirect()->route('contributor_list', [$this->podcast->id]);
    }

    public function remove()
    {
        $authorize = Services::authorization();

        $group_model = new GroupModel();

        $group = $group_model
            ->select('auth_groups.*')
            ->join(
                'auth_groups_users',
                'auth_groups_users.group_id = auth_groups.id'
            )
            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
            ->where('user_id', $this->user->id)
            ->first();

        $authorize->removeUserFromGroup(
            (int) $this->user->id,
            (int) $group->id
        );

        (new PodcastModel())->removeContributorFromPodcast(
            $this->user->id,
            $this->podcast->id
        );

        return redirect()->route('contributor_list', [$this->podcast->id]);
    }
}
+135 −98
Original line number Diff line number Diff line
@@ -17,23 +17,52 @@ class Episode extends BaseController

    public function _remap($method, ...$params)
    {
        switch ($method) {
            case 'list':
                if (
                    !has_permission('episodes-list') ||
                    !has_permission("podcasts:$params[0]:episodes-list")
                ) {
                    throw new \RuntimeException(
                        lang('Auth.notEnoughPrivilege')
                    );
                }
            case 'edit':
                if (
                    !has_permission('episodes-edit') ||
                    !has_permission("podcasts:$params[0]:episodes-edit")
                ) {
                    throw new \RuntimeException(
                        lang('Auth.notEnoughPrivilege')
                    );
                }
            case 'delete':
                if (
                    !has_permission('episodes-delete') ||
                    !has_permission("podcasts:$params[0]:episodes-delete")
                ) {
                    throw new \RuntimeException(
                        lang('Auth.notEnoughPrivilege')
                    );
                }
        }

        $podcast_model = new PodcastModel();

        $this->podcast = $podcast_model->where('name', $params[0])->first();
        $this->podcast = $podcast_model->find($params[0]);

        if (count($params) > 1) {
            $episode_model = new EpisodeModel();
            if (
                !($episode = $episode_model
                !($this->episode = $episode_model
                    ->where([
                        'podcast_id' => $this->podcast->id,
                        'slug' => $params[1],
                        'id' => $params[1],
                        'podcast_id' => $params[0],
                    ])
                    ->first())
            ) {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
            $this->episode = $episode;
        }

        return $this->$method();
@@ -41,13 +70,8 @@ class Episode extends BaseController

    public function list()
    {
        $episode_model = new EpisodeModel();

        $data = [
            'podcast' => $this->podcast,
            'all_podcast_episodes' => $episode_model
                ->where('podcast_id', $this->podcast->id)
                ->find(),
        ];

        return view('admin/episode/list', $data);
@@ -57,105 +81,118 @@ class Episode extends BaseController
    {
        helper(['form']);

        if (
            !$this->validate([
                'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
                'image' =>
                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
                'title' => 'required',
                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
                'description' => 'required',
                'type' => 'required',
            ])
        ) {
        $data = [
            'podcast' => $this->podcast,
        ];

        echo view('admin/episode/create', $data);
        } else {
    }

    public function attemptCreate()
    {
        $rules = [
            'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
            'image' =>
                'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
        ];

        if (!$this->validate($rules)) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $this->validator->getErrors());
        }

        $new_episode = new \App\Entities\Episode([
            'podcast_id' => $this->podcast->id,
                'title' => $this->request->getVar('title'),
                'slug' => $this->request->getVar('slug'),
            'title' => $this->request->getPost('title'),
            'slug' => $this->request->getPost('slug'),
            'enclosure' => $this->request->getFile('enclosure'),
                'pub_date' => $this->request->getVar('pub_date'),
                'description' => $this->request->getVar('description'),
            'pub_date' => $this->request->getPost('pub_date'),
            'description' => $this->request->getPost('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,
            'explicit' => (bool) $this->request->getPost('explicit'),
            'number' => $this->request->getPost('episode_number'),
            'season_number' => $this->request->getPost('season_number'),
            'type' => $this->request->getPost('type'),
            'author_name' => $this->request->getPost('author_name'),
            'author_email' => $this->request->getPost('author_email'),
            'block' => (bool) $this->request->getPost('block'),
        ]);

        $episode_model = new EpisodeModel();
            $episode_model->save($new_episode);

            return redirect()->route('episode_list', [$this->podcast->name]);
        if (!$episode_model->save($new_episode)) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $episode_model->errors());
        }

        return redirect()->route('episode_list', [$this->podcast->id]);
    }

    public function edit()
    {
        helper(['form']);

        if (
            !$this->validate([
                'enclosure' =>
                    'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
                'image' =>
                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
                'title' => 'required',
                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
                'description' => 'required',
                'type' => 'required',
            ])
        ) {
        $data = [
            'podcast' => $this->podcast,
            'episode' => $this->episode,
        ];

        echo view('admin/episode/edit', $data);
        } else {
            $this->episode->title = $this->request->getVar('title');
            $this->episode->slug = $this->request->getVar('slug');
            $this->episode->pub_date = $this->request->getVar('pub_date');
            $this->episode->description = $this->request->getVar('description');
            $this->episode->explicit =
                ($this->request->getVar('explicit') or false);
            $this->episode->number = $this->request->getVar('episode_number');
            $this->episode->season_number = $this->request->getVar(
                'season_number'
            )
                ? $this->request->getVar('season_number')
    }

    public function attemptEdit()
    {
        $rules = [
            'enclosure' =>
                'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
            'image' =>
                'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
        ];

        if (!$this->validate($rules)) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $this->validator->getErrors());
        }

        $this->episode->title = $this->request->getPost('title');
        $this->episode->slug = $this->request->getPost('slug');
        $this->episode->pub_date = $this->request->getPost('pub_date');
        $this->episode->description = $this->request->getPost('description');
        $this->episode->explicit = (bool) $this->request->getPost('explicit');
        $this->episode->number = $this->request->getPost('episode_number');
        $this->episode->season_number = $this->request->getPost('season_number')
            ? $this->request->getPost('season_number')
            : null;
            $this->episode->type = $this->request->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);
        $this->episode->type = $this->request->getPost('type');
        $this->episode->author_name = $this->request->getPost('author_name');
        $this->episode->author_email = $this->request->getPost('author_email');
        $this->episode->block = (bool) $this->request->getPost('block');

        $enclosure = $this->request->getFile('enclosure');
        if ($enclosure->isValid()) {
                $this->episode->enclosure = $this->request->getFile(
                    'enclosure'
                );
            $this->episode->enclosure = $enclosure;
        }
        $image = $this->request->getFile('image');
        if ($image) {
                $this->episode->image = $this->request->getFile('image');
            $this->episode->image = $image;
        }

        $episode_model = new EpisodeModel();
            $episode_model->save($this->episode);

            return redirect()->route('episode_list', [$this->podcast->name]);
        if (!$episode_model->save($this->episode)) {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $episode_model->errors());
        }

        return redirect()->route('episode_list', [$this->podcast->id]);
    }

    public function delete()
@@ -163,6 +200,6 @@ class Episode extends BaseController
        $episode_model = new EpisodeModel();
        $episode_model->delete($this->episode->id);

        return redirect()->route('episode_list', [$this->podcast->name]);
        return redirect()->route('episode_list', [$this->podcast->id]);
    }
}
+166 −118

File changed.

Preview size limit exceeded, changes collapsed.

+30 −25
Original line number Diff line number Diff line
@@ -17,12 +17,9 @@ class User extends BaseController
    {
        if (count($params) > 0) {
            $user_model = new UserModel();
            if (
                !($user = $user_model->where('username', $params[0])->first())
            ) {
            if (!($this->user = $user_model->find($params[0]))) {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
            $this->user = $user;
        }

        return $this->$method();
@@ -38,6 +35,11 @@ class User extends BaseController
    }

    public function create()
    {
        echo view('admin/user/create');
    }

    public function attemptCreate()
    {
        $user_model = new UserModel();

@@ -53,8 +55,12 @@ class User extends BaseController
        );

        if (!$this->validate($rules)) {
            echo view('admin/user/create');
        } else {
            return redirect()
                ->back()
                ->withInput()
                ->with('errors', $this->validator->getErrors());
        }

        // Save the user
        $user = new \Myth\Auth\Entities\User($this->request->getPost());

@@ -77,7 +83,6 @@ class User extends BaseController
            ->route('user_list')
            ->with('message', lang('User.createSuccess'));
    }
    }

    public function forcePassReset()
    {
Loading