diff --git a/app/Authorization/FlatAuthorization.php b/app/Authorization/FlatAuthorization.php
new file mode 100644
index 0000000000000000000000000000000000000000..48b4ad735145cfa527d419f338321f08d5cfafe7
--- /dev/null
+++ b/app/Authorization/FlatAuthorization.php
@@ -0,0 +1,76 @@
+<?php namespace App\Authorization;
+
+class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
+{
+    //--------------------------------------------------------------------
+    // Actions
+    //--------------------------------------------------------------------
+
+    /**
+     * Checks a group to see if they have the specified permission.
+     *
+     * @param int|string $permission
+     * @param int        $groupId
+     *
+     * @return mixed
+     */
+    public function groupHasPermission($permission, int $groupId)
+    {
+        if (
+            empty($permission) ||
+            (!is_string($permission) && !is_numeric($permission))
+        ) {
+            return null;
+        }
+
+        if (empty($groupId) || !is_numeric($groupId)) {
+            return null;
+        }
+
+        // Get the Permission ID
+        $permissionId = $this->getPermissionID($permission);
+
+        if (!is_numeric($permissionId)) {
+            return false;
+        }
+
+        if (
+            $this->permissionModel->doesGroupHavePermission(
+                $groupId,
+                (int) $permissionId
+            )
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Makes a member a part of multiple groups.
+     *
+     * @param $user_id
+     * @param array|null $groups // Either collection of ID or names
+     *
+     * @return bool
+     */
+    public function setUserGroups(int $user_id, $groups)
+    {
+        if (empty($user_id) || !is_numeric($user_id)) {
+            return null;
+        }
+
+        // remove user from all groups before resetting it in new groups
+        $this->groupModel->removeUserFromAllGroups($user_id);
+
+        if (empty($groups)) {
+            return true;
+        }
+
+        foreach ($groups as $group) {
+            $this->addUserToGroup($user_id, $group);
+        }
+
+        return true;
+    }
+}
diff --git a/app/Authorization/GroupModel.php b/app/Authorization/GroupModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..597a54f155bf95a433ffc65f79d69918981b9aa9
--- /dev/null
+++ b/app/Authorization/GroupModel.php
@@ -0,0 +1,18 @@
+<?php namespace App\Authorization;
+
+class GroupModel extends \Myth\Auth\Authorization\GroupModel
+{
+    public function getContributorRoles()
+    {
+        return $this->select('auth_groups.*')
+            ->like('name', 'podcast_', 'after')
+            ->findAll();
+    }
+
+    public function getUserRoles()
+    {
+        return $this->select('auth_groups.*')
+            ->notLike('name', 'podcast_', 'after')
+            ->findAll();
+    }
+}
diff --git a/app/Authorization/PermissionModel.php b/app/Authorization/PermissionModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..b8b56274f6c7d5b283f47eca01ed2a7e20a8c23f
--- /dev/null
+++ b/app/Authorization/PermissionModel.php
@@ -0,0 +1,63 @@
+<?php namespace App\Authorization;
+
+class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
+{
+    /**
+     * Checks to see if a user, or one of their groups,
+     * has a specific permission.
+     *
+     * @param $userId
+     * @param $permissionId
+     *
+     * @return bool
+     */
+    public function doesGroupHavePermission(
+        int $groupId,
+        int $permissionId
+    ): bool {
+        // Check group permissions and take advantage of caching
+        $groupPerms = $this->getPermissionsForGroup($groupId);
+
+        return count($groupPerms) &&
+            array_key_exists($permissionId, $groupPerms);
+    }
+
+    /**
+     * Gets all permissions for a group in a way that can be
+     * easily used to check against:
+     *
+     * [
+     *  id => name,
+     *  id => name
+     * ]
+     *
+     * @param int $groupId
+     *
+     * @return array
+     */
+    public function getPermissionsForGroup(int $groupId): array
+    {
+        if (!($found = cache("group{$groupId}_permissions"))) {
+            $groupPermissions = $this->db
+                ->table('auth_groups_permissions')
+                ->select('id, auth_permissions.name')
+                ->join(
+                    'auth_permissions',
+                    'auth_permissions.id = permission_id',
+                    'inner'
+                )
+                ->where('group_id', $groupId)
+                ->get()
+                ->getResultObject();
+
+            $found = [];
+            foreach ($groupPermissions as $row) {
+                $found[$row->id] = strtolower($row->name);
+            }
+
+            cache()->save("group{$groupId}_permissions", $found, 300);
+        }
+
+        return $found;
+    }
+}
diff --git a/app/Config/Filters.php b/app/Config/Filters.php
index fef58ff5f231148264a89e47dfff526b4bf7cbfa..da69ca64af718ca3fdabc4cbc4c1d862ec025697 100644
--- a/app/Config/Filters.php
+++ b/app/Config/Filters.php
@@ -12,7 +12,7 @@ class Filters extends BaseConfig
         'honeypot' => \CodeIgniter\Filters\Honeypot::class,
         'login' => \Myth\Auth\Filters\LoginFilter::class,
         'role' => \Myth\Auth\Filters\RoleFilter::class,
-        'permission' => \Myth\Auth\Filters\PermissionFilter::class,
+        'permission' => \App\Filters\Permission::class,
     ];
 
     // Always applied before every request
diff --git a/app/Config/Paths.php b/app/Config/Paths.php
index 9c126be19f8733739e99b4f410cd02393d627e6d..f574dad59d5e50aa0837f758c44896aece142ce4 100644
--- a/app/Config/Paths.php
+++ b/app/Config/Paths.php
@@ -21,7 +21,7 @@ class Paths
      * as this file.
      */
     public $systemDirectory =
-        __DIR__ . '/../../vendor/codeigniter4/framework/system';
+        __DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
 
     /*
      *---------------------------------------------------------------
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 552f33a6fb88aface994bc4aa3196bfe45b36104..84a96218aca70886e2e5c6cbf0066bfc986b9d55 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -22,6 +22,13 @@ $routes->setDefaultMethod('index');
 $routes->setTranslateURIDashes(false);
 $routes->set404Override();
 $routes->setAutoRoute(false);
+
+/**
+ * --------------------------------------------------------------------
+ * Placeholder definitions
+ * --------------------------------------------------------------------
+ */
+
 $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,}');
@@ -64,9 +71,11 @@ $routes->group(
             'as' => 'admin_home',
         ]);
 
+        $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',
@@ -80,58 +89,97 @@ $routes->group(
         $routes->group('podcasts/(:num)', function ($routes) {
             $routes->get('/', 'Podcast::view/$1', [
                 'as' => 'podcast_view',
+                'filter' => 'permission:podcasts-view,podcast-view',
             ]);
             $routes->get('edit', 'Podcast::edit/$1', [
                 'as' => 'podcast_edit',
+                'filter' => 'permission:podcasts-edit,podcast-edit',
+            ]);
+            $routes->post('edit', 'Podcast::attemptEdit/$1', [
+                'filter' => 'permission:podcasts-edit,podcast-edit',
             ]);
-            $routes->post('edit', 'Podcast::attemptEdit/$1');
             $routes->add('delete', 'Podcast::delete/$1', [
                 'as' => 'podcast_delete',
+                'filter' => 'permission:podcasts-edit,podcast-delete',
             ]);
 
             // Podcast episodes
             $routes->get('episodes', 'Episode::list/$1', [
                 'as' => 'episode_list',
+                'filter' => 'permission:podcasts-view,podcast-view',
             ]);
             $routes->get('new-episode', 'Episode::create/$1', [
                 'as' => 'episode_create',
+                'filter' =>
+                    'permission:episodes-create,podcast_episodes-create',
+            ]);
+            $routes->post('new-episode', 'Episode::attemptCreate/$1', [
+                'filter' =>
+                    'permission:episodes-create,podcast_episodes-create',
             ]);
-            $routes->post('new-episode', 'Episode::attemptCreate/$1');
 
             $routes->get('episodes/(:num)', 'Episode::view/$1/$2', [
                 'as' => 'episode_view',
+                'filter' => 'permission:episodes-list,podcast_episodes-list',
             ]);
             $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
                 'as' => 'episode_edit',
+                'filter' => 'permission:episodes-edit,podcast_episodes-edit',
             ]);
-            $routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2');
+            $routes->post(
+                'episodes/(:num)/edit',
+                'Episode::attemptEdit/$1/$2',
+                [
+                    'filter' =>
+                        'permission:episodes-edit,podcast_episodes-edit',
+                ]
+            );
             $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [
                 'as' => 'episode_delete',
+                'filter' =>
+                    'permission:episodes-delete,podcast_episodes-delete',
             ]);
 
             // Podcast contributors
             $routes->get('contributors', 'Contributor::list/$1', [
                 'as' => 'contributor_list',
+                'filter' =>
+                    'permission:podcasts-manage_contributors,podcast-manage_contributors',
             ]);
             $routes->get('add-contributor', 'Contributor::add/$1', [
                 'as' => 'contributor_add',
+                'filter' =>
+                    'permission:podcasts-manage_contributors,podcast-manage_contributors',
+            ]);
+            $routes->post('add-contributor', 'Contributor::attemptAdd/$1', [
+                'filter' =>
+                    'permission:podcasts-manage_contributors,podcast-manage_contributors',
             ]);
-            $routes->post('add-contributor', 'Contributor::attemptAdd/$1');
             $routes->get(
                 'contributors/(:num)/edit',
                 'Contributor::edit/$1/$2',
                 [
                     'as' => 'contributor_edit',
+                    'filter' =>
+                        'permission:podcasts-manage_contributors,podcast-manage_contributors',
                 ]
             );
             $routes->post(
                 'contributors/(:num)/edit',
-                'Contributor::attemptEdit/$1/$2'
+                'Contributor::attemptEdit/$1/$2',
+                [
+                    'filter' =>
+                        'permission:podcasts-manage_contributors,podcast-manage_contributors',
+                ]
             );
             $routes->add(
                 'contributors/(:num)/remove',
                 'Contributor::remove/$1/$2',
-                ['as' => 'contributor_remove']
+                [
+                    'as' => 'contributor_remove',
+                    'filter' =>
+                        'permission:podcasts-manage_contributors,podcast-manage_contributors',
+                ]
             );
         });
 
@@ -147,6 +195,13 @@ $routes->group(
         $routes->post('new-user', 'User::attemptCreate', [
             'filter' => 'permission:users-create',
         ]);
+        $routes->get('users/(:num)/edit', 'User::edit/$1', [
+            'as' => 'user_edit',
+            'filter' => 'permission:users-manage_authorizations',
+        ]);
+        $routes->post('users/(:num)/edit', 'User::attemptEdit/$1', [
+            'filter' => 'permission:users-manage_authorizations',
+        ]);
 
         $routes->add('users/(:num)/ban', 'User::ban/$1', [
             'as' => 'user_ban',
@@ -194,40 +249,44 @@ $routes->group(
 /**
  * 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');
+$routes->group(
+    config('App')->authGateway,
+    ['namespace' => 'Myth\Auth\Controllers'],
+    function ($routes) {
+        // Login/out
+        $routes->get('login', 'AuthController::login', ['as' => 'login']);
+        $routes->post('login', 'AuthController::attemptLogin');
+        $routes->get('logout', 'AuthController::logout', ['as' => 'logout']);
 
-    // Activation
-    $routes->get('activate-account', 'Auth::activateAccount', [
-        'as' => 'activate-account',
-    ]);
-    $routes->get('resend-activate-account', 'Auth::resendActivateAccount', [
-        'as' => 'resend-activate-account',
-    ]);
+        // Registration
+        $routes->get('register', 'AuthController::register', [
+            'as' => 'register',
+        ]);
+        $routes->post('register', 'AuthController::attemptRegister');
 
-    // 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');
-});
+        // Activation
+        $routes->get('activate-account', 'AuthController::activateAccount', [
+            'as' => 'activate-account',
+        ]);
+        $routes->get(
+            'resend-activate-account',
+            'AuthController::resendActivateAccount',
+            [
+                'as' => 'resend-activate-account',
+            ]
+        );
+
+        // Forgot/Resets
+        $routes->get('forgot', 'AuthController::forgotPassword', [
+            'as' => 'forgot',
+        ]);
+        $routes->post('forgot', 'Auth::attemptForgot');
+        $routes->get('reset-password', 'AuthController::resetPassword', [
+            'as' => 'reset-password',
+        ]);
+        $routes->post('reset-password', 'AuthController::attemptReset');
+    }
+);
 
 /**
  * --------------------------------------------------------------------
diff --git a/app/Config/Services.php b/app/Config/Services.php
index 2fe37be6aa10405c3c1b9035e1eeb971eccdcfee..634c54608e366f70a33878eef886ed39ee7eff17 100644
--- a/app/Config/Services.php
+++ b/app/Config/Services.php
@@ -1,6 +1,12 @@
 <?php namespace Config;
 
 use CodeIgniter\Config\Services as CoreServices;
+use CodeIgniter\Model;
+use App\Authorization\FlatAuthorization;
+use App\Authorization\PermissionModel;
+use App\Authorization\GroupModel;
+use App\Models\UserModel;
+use Myth\Auth\Models\LoginModel;
 
 require_once SYSTEMPATH . 'Config/Services.php';
 
@@ -19,11 +25,68 @@ require_once SYSTEMPATH . 'Config/Services.php';
  */
 class Services extends CoreServices
 {
-    // public static function example($getShared = true)
-// {
-//     if ($getShared) {
-//         return static::getSharedInstance('example');
-//     }
-//     return new \CodeIgniter\Example();
-// }
+    public static function authentication(
+        string $lib = 'local',
+        Model $userModel = null,
+        Model $loginModel = null,
+        bool $getShared = true
+    ) {
+        if ($getShared) {
+            return self::getSharedInstance(
+                'authentication',
+                $lib,
+                $userModel,
+                $loginModel
+            );
+        }
+
+        // config() checks first in app/Config
+        $config = config('Auth');
+
+        $class = $config->authenticationLibs[$lib];
+
+        $instance = new $class($config);
+
+        if (empty($userModel)) {
+            $userModel = new UserModel();
+        }
+
+        if (empty($loginModel)) {
+            $loginModel = new LoginModel();
+        }
+
+        return $instance->setUserModel($userModel)->setLoginModel($loginModel);
+    }
+
+    public static function authorization(
+        Model $groupModel = null,
+        Model $permissionModel = null,
+        Model $userModel = null,
+        bool $getShared = true
+    ) {
+        if ($getShared) {
+            return self::getSharedInstance(
+                'authorization',
+                $groupModel,
+                $permissionModel,
+                $userModel
+            );
+        }
+
+        if (is_null($groupModel)) {
+            $groupModel = new GroupModel();
+        }
+
+        if (is_null($permissionModel)) {
+            $permissionModel = new PermissionModel();
+        }
+
+        $instance = new FlatAuthorization($groupModel, $permissionModel);
+
+        if (is_null($userModel)) {
+            $userModel = new UserModel();
+        }
+
+        return $instance->setUserModel($userModel);
+    }
 }
diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php
index 1cf39aee2ae21de24575e26115c2b68634b1e64b..708912f73b6010ef6201f43c65fdee5539736a9f 100644
--- a/app/Controllers/Admin/Contributor.php
+++ b/app/Controllers/Admin/Contributor.php
@@ -7,43 +7,25 @@
 
 namespace App\Controllers\Admin;
 
+use App\Authorization\GroupModel;
 use App\Models\PodcastModel;
-use Myth\Auth\Authorization\GroupModel;
-use Myth\Auth\Config\Services;
-use Myth\Auth\Models\UserModel;
+use App\Models\UserModel;
 
 class Contributor extends BaseController
 {
     protected \App\Entities\Podcast $podcast;
-    protected ?\Myth\Auth\Entities\User $user;
+    protected ?\App\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]);
+        $this->podcast = (new PodcastModel())->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())
+                !($this->user = (new UserModel())->getPodcastContributor(
+                    $params[1],
+                    $params[0]
+                ))
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
@@ -63,18 +45,10 @@ class Contributor extends BaseController
 
     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,
+            'users' => (new UserModel())->findAll(),
+            'roles' => (new GroupModel())->getContributorRoles(),
         ];
 
         echo view('admin/contributor/add', $data);
@@ -82,46 +56,32 @@ class Contributor extends BaseController
 
     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
-        );
+        try {
+            (new PodcastModel())->addPodcastContributor(
+                $this->request->getPost('user'),
+                $this->podcast->id,
+                $this->request->getPost('role')
+            );
+        } catch (\Exception $e) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', [lang('Contributor.alreadyAddedError')]);
+        }
 
         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,
+            'contributor_group_id' => (new PodcastModel())->getContributorGroupId(
+                $this->user->id,
+                $this->podcast->id
+            ),
+            'roles' => (new GroupModel())->getContributorRoles(),
         ];
 
         echo view('admin/contributor/edit', $data);
@@ -129,28 +89,10 @@ class Contributor extends BaseController
 
     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')
+        (new PodcastModel())->updatePodcastContributor(
+            $this->user->id,
+            $this->podcast->id,
+            $this->request->getPost('role')
         );
 
         return redirect()->route('contributor_list', [$this->podcast->id]);
@@ -158,30 +100,34 @@ class Contributor extends BaseController
 
     public function remove()
     {
-        $authorize = Services::authorization();
-
-        $group_model = new GroupModel();
+        if ($this->podcast->owner_id == $this->user->id) {
+            return redirect()
+                ->back()
+                ->with('errors', [
+                    lang('Contributor.removeOwnerContributorError'),
+                ]);
+        }
 
-        $group = $group_model
-            ->select('auth_groups.*')
-            ->join(
-                'auth_groups_users',
-                'auth_groups_users.group_id = auth_groups.id'
+        $podcast_model = new PodcastModel();
+        if (
+            !$podcast_model->removePodcastContributor(
+                $this->user->id,
+                $this->podcast->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()
+                ->back()
+                ->with('errors', $podcast_model->errors());
+        }
 
-        return redirect()->route('contributor_list', [$this->podcast->id]);
+        return redirect()
+            ->back()
+            ->with(
+                'message',
+                lang('Contributor.removeContributorSuccess', [
+                    'username' => $this->user->username,
+                    'podcastTitle' => $this->podcast->title,
+                ])
+            );
     }
 }
diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php
index 70964117aef50c604bddf316c0777b1cb765cc8f..74efa4362d1b1fd4e007b0f542fedf69c487af34 100644
--- a/app/Controllers/Admin/Episode.php
+++ b/app/Controllers/Admin/Episode.php
@@ -17,39 +17,7 @@ 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->find($params[0]);
+        $this->podcast = (new PodcastModel())->find($params[0]);
 
         if (count($params) > 1) {
             $episode_model = new EpisodeModel();
diff --git a/app/Controllers/Admin/Myaccount.php b/app/Controllers/Admin/Myaccount.php
index 5cd23535dd5c6fa39d97d78b9856efad775f41a5..4f3f75719223ef11fd40a5c654db8abf4d3b64a1 100644
--- a/app/Controllers/Admin/Myaccount.php
+++ b/app/Controllers/Admin/Myaccount.php
@@ -7,8 +7,8 @@
 
 namespace App\Controllers\Admin;
 
-use Myth\Auth\Config\Services;
-use Myth\Auth\Models\UserModel;
+use Config\Services;
+use App\Models\UserModel;
 
 class Myaccount extends BaseController
 {
diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php
index 8259902fe8f20ca9adfd780f9c2b9d2855017d11..4b99340cd47a6979da60e1457fe298358739221a 100644
--- a/app/Controllers/Admin/Podcast.php
+++ b/app/Controllers/Admin/Podcast.php
@@ -9,6 +9,7 @@ namespace App\Controllers\Admin;
 use App\Models\CategoryModel;
 use App\Models\LanguageModel;
 use App\Models\PodcastModel;
+use Config\Services;
 
 class Podcast extends BaseController
 {
@@ -17,38 +18,7 @@ class Podcast extends BaseController
     public function _remap($method, ...$params)
     {
         if (count($params) > 0) {
-            switch ($method) {
-                case 'view':
-                    if (
-                        !has_permission('podcasts-view') ||
-                        !has_permission("podcasts:$params[0]-view")
-                    ) {
-                        throw new \RuntimeException(
-                            lang('Auth.notEnoughPrivilege')
-                        );
-                    }
-                case 'edit':
-                    if (
-                        !has_permission('podcasts-edit') ||
-                        !has_permission("podcasts:$params[0]-edit")
-                    ) {
-                        throw new \RuntimeException(
-                            lang('Auth.notEnoughPrivilege')
-                        );
-                    }
-                case 'delete':
-                    if (
-                        !has_permission('podcasts-delete') ||
-                        !has_permission("podcasts:$params[0]-delete")
-                    ) {
-                        throw new \RuntimeException(
-                            lang('Auth.notEnoughPrivilege')
-                        );
-                    }
-            }
-
-            $podcast_model = new PodcastModel();
-            if (!($this->podcast = $podcast_model->find($params[0]))) {
+            if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
         }
@@ -56,18 +26,22 @@ class Podcast extends BaseController
         return $this->$method();
     }
 
-    public function list()
+    public function myPodcasts()
     {
-        $podcast_model = new PodcastModel();
+        $data = [
+            'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
+        ];
 
-        $all_podcasts = [];
-        if (has_permission('podcasts-list')) {
-            $all_podcasts = $podcast_model->findAll();
-        } else {
-            $all_podcasts = $podcast_model->getUserPodcasts(user()->id);
+        return view('admin/podcast/list', $data);
+    }
+
+    public function list()
+    {
+        if (!has_permission('podcasts-list')) {
+            return redirect()->route('my_podcasts');
         }
 
-        $data = ['all_podcasts' => $all_podcasts];
+        $data = ['all_podcasts' => (new PodcastModel())->findAll()];
 
         return view('admin/podcast/list', $data);
     }
@@ -145,7 +119,14 @@ class Podcast extends BaseController
                 ->with('errors', $podcast_model->errors());
         }
 
-        $podcast_model->addContributorToPodcast(user()->id, $new_podcast_id);
+        $authorize = Services::authorization();
+        $podcast_admin_group = $authorize->group('podcast_admin');
+
+        $podcast_model->addPodcastContributor(
+            user()->id,
+            $new_podcast_id,
+            $podcast_admin_group->id
+        );
 
         $db->transComplete();
 
diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php
index 4a8342f50a37ee220d3c9f612b3fc50005a35d7e..f958fe2edd235d1fad8d0dd6963ca12872ee00f9 100644
--- a/app/Controllers/Admin/User.php
+++ b/app/Controllers/Admin/User.php
@@ -7,11 +7,13 @@
 
 namespace App\Controllers\Admin;
 
-use Myth\Auth\Models\UserModel;
+use App\Authorization\GroupModel;
+use App\Models\UserModel;
+use Config\Services;
 
 class User extends BaseController
 {
-    protected ?\Myth\Auth\Entities\User $user;
+    protected ?\App\Entities\User $user;
 
     public function _remap($method, ...$params)
     {
@@ -27,16 +29,18 @@ class User extends BaseController
 
     public function list()
     {
-        $user_model = new UserModel();
-
-        $data = ['all_users' => $user_model->findAll()];
+        $data = ['all_users' => (new UserModel())->findAll()];
 
         return view('admin/user/list', $data);
     }
 
     public function create()
     {
-        echo view('admin/user/create');
+        $data = [
+            'roles' => (new GroupModel())->getUserRoles(),
+        ];
+
+        echo view('admin/user/create', $data);
     }
 
     public function attemptCreate()
@@ -62,14 +66,13 @@ class User extends BaseController
         }
 
         // Save the user
-        $user = new \Myth\Auth\Entities\User($this->request->getPost());
+        $user = new \App\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();
+        $user->forcePasswordReset();
 
         if (!$user_model->save($user)) {
             return redirect()
@@ -81,15 +84,46 @@ class User extends BaseController
         // Success!
         return redirect()
             ->route('user_list')
-            ->with('message', lang('User.createSuccess'));
+            ->with(
+                'message',
+                lang('User.createSuccess', [
+                    'username' => $user->username,
+                ])
+            );
+    }
+
+    public function edit()
+    {
+        $data = [
+            'user' => $this->user,
+            'roles' => (new GroupModel())->getUserRoles(),
+        ];
+
+        echo view('admin/user/edit', $data);
+    }
+
+    public function attemptEdit()
+    {
+        $authorize = Services::authorization();
+
+        $roles = $this->request->getPost('roles');
+        $authorize->setUserGroups($this->user->id, $roles);
+
+        // Success!
+        return redirect()
+            ->route('user_list')
+            ->with(
+                'message',
+                lang('User.rolesEditSuccess', [
+                    'username' => $this->user->username,
+                ])
+            );
     }
 
     public function forcePassReset()
     {
         $user_model = new UserModel();
-
-        $this->user->force_pass_reset = true;
-        $this->user->generateResetHash();
+        $this->user->forcePasswordReset();
 
         if (!$user_model->save($this->user)) {
             return redirect()
@@ -100,12 +134,29 @@ class User extends BaseController
         // Success!
         return redirect()
             ->route('user_list')
-            ->with('message', lang('User.forcePassResetSuccess'));
+            ->with(
+                'message',
+                lang('User.forcePassResetSuccess', [
+                    'username' => $this->user->username,
+                ])
+            );
     }
 
     public function ban()
     {
+        $authorize = Services::authorization();
+        if ($authorize->inGroup('superadmin', $this->user->id)) {
+            return redirect()
+                ->back()
+                ->with('errors', [
+                    lang('User.banSuperAdminError', [
+                        'username' => $this->user->username,
+                    ]),
+                ]);
+        }
+
         $user_model = new UserModel();
+        // TODO: add ban reason?
         $this->user->ban('');
 
         if (!$user_model->save($this->user)) {
@@ -116,7 +167,12 @@ class User extends BaseController
 
         return redirect()
             ->route('user_list')
-            ->with('message', lang('User.banSuccess'));
+            ->with(
+                'message',
+                lang('User.banSuccess', [
+                    'username' => $this->user->username,
+                ])
+            );
     }
 
     public function unBan()
@@ -132,16 +188,37 @@ class User extends BaseController
 
         return redirect()
             ->route('user_list')
-            ->with('message', lang('User.unbanSuccess'));
+            ->with(
+                'message',
+                lang('User.unbanSuccess', [
+                    'username' => $this->user->username,
+                ])
+            );
     }
 
     public function delete()
     {
+        $authorize = Services::authorization();
+        if ($authorize->inGroup('superadmin', $this->user->id)) {
+            return redirect()
+                ->back()
+                ->with('errors', [
+                    lang('User.deleteSuperAdminError', [
+                        'username' => $this->user->username,
+                    ]),
+                ]);
+        }
+
         $user_model = new UserModel();
         $user_model->delete($this->user->id);
 
         return redirect()
-            ->route('user_list')
-            ->with('message', lang('User.deleteSuccess'));
+            ->back()
+            ->with(
+                'message',
+                lang('User.deleteSuccess', [
+                    'username' => $this->user->username,
+                ])
+            );
     }
 }
diff --git a/app/Controllers/Auth.php b/app/Controllers/Auth.php
deleted file mode 100644
index f91900654f22fab18ab106335ecd2cdf5f7d6e26..0000000000000000000000000000000000000000
--- a/app/Controllers/Auth.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?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/Database/Migrations/2020-07-03-191500_add_users_podcasts.php b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php
index abed46d5166f9a33ddd356aa48297e487bb3716d..5b731576cb81cce67731e6a1296ffc85349f2b49 100644
--- a/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php
+++ b/app/Database/Migrations/2020-07-03-191500_add_users_podcasts.php
@@ -27,10 +27,16 @@ class AddUsersPodcasts extends Migration
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'group_id' => [
+                'type' => 'INT',
+                'constraint' => 11,
+                'unsigned' => true,
+            ],
         ]);
         $this->forge->addPrimaryKey(['user_id', 'podcast_id']);
         $this->forge->addForeignKey('user_id', 'users', 'id');
         $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->addForeignKey('group_id', 'auth_groups', 'id');
         $this->forge->createTable('users_podcasts');
     }
 
diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php
index af0eeab02e71103e1f876101132f33031714cb63..1c60c4271e7c1eb3e465ccddbb2391d9b6970ab8 100644
--- a/app/Database/Seeds/AuthSeeder.php
+++ b/app/Database/Seeds/AuthSeeder.php
@@ -14,141 +14,267 @@ use CodeIgniter\Database\Seeder;
 
 class AuthSeeder extends Seeder
 {
-    public function run()
-    {
-        helper('auth');
+    protected $groups = [
+        [
+            'name' => 'superadmin',
+            'description' =>
+                'Somebody who has access to all the castopod instance features',
+        ],
+        [
+            'name' => 'podcast_admin',
+            'description' =>
+                'Somebody who has access to all the features within a given podcast',
+        ],
+    ];
+
+    /** Build permissions array as a list of:
+     *
+     * ```
+     * context => [
+     *      [action, description],
+     *      [action, description],
+     *      ...
+     * ]
+     * ```
+     */
+    protected $permissions = [
+        'users' => [
+            [
+                'name' => 'create',
+                'description' => 'Create a user',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'list',
+                'description' => 'List all users',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'manage_authorizations',
+                'description' => 'Add or remove roles/permissions to a user',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'manage_bans',
+                'description' => 'Ban / unban a user',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'force_pass_reset',
+                'description' =>
+                    'Force a user to update his password upon next login',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete',
+                'description' =>
+                    'Delete user without removing him from database',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' =>
+                    'Delete all occurrences of a user from the database',
+                'has_permission' => ['superadmin'],
+            ],
+        ],
+        'podcasts' => [
+            [
+                'name' => 'create',
+                'description' => 'Add a new podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'list',
+                'description' => 'List all podcasts and their episodes',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'view',
+                'description' => 'View any podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'edit',
+                'description' => 'Edit any podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'manage_contributors',
+                'description' => 'Add / remove contributors to a podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'manage_publication',
+                'description' => 'Publish / unpublish a podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete',
+                'description' =>
+                    'Delete a podcast without removing it from database',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' => 'Delete any podcast from the database',
+                'has_permission' => ['superadmin'],
+            ],
+        ],
+        'episodes' => [
+            [
+                'name' => 'list',
+                'description' => 'List all episodes of any podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'create',
+                'description' => 'Add a new episode to any podcast',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'edit',
+                'description' => 'Edit any podcast episode',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'manage_publications',
+                'description' => 'Publish / unpublish any podcast episode',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete',
+                'description' =>
+                    'Delete any podcast episode without removing it from database',
+                'has_permission' => ['superadmin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' => 'Delete any podcast episode from database',
+                'has_permission' => ['superadmin'],
+            ],
+        ],
+        'podcast' => [
+            [
+                'name' => 'view',
+                'description' => 'View a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'edit',
+                'description' => 'Edit a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'delete',
+                'description' =>
+                    'Delete a podcast without removing it from the database',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' => 'Delete a podcast from the database',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'manage_contributors',
+                'description' =>
+                    'Add / remove contributors to a podcast and edit their roles',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'manage_publication',
+                'description' => 'Publish / unpublish a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+        ],
+        'podcast_episodes' => [
+            [
+                'name' => 'list',
+                'description' => 'List all episodes of a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'create',
+                'description' => 'Add new episodes for a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'edit',
+                'description' => 'Edit an episode of a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'delete',
+                'description' =>
+                    'Delete an episode of a podcast without removing it from the database',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'delete_permanently',
+                'description' =>
+                    'Delete all occurrences of an episode of a podcast from the database',
+                'has_permission' => ['podcast_admin'],
+            ],
+            [
+                'name' => 'manage_publications',
+                'description' => 'Publish / unpublish episodes of a podcast',
+                'has_permission' => ['podcast_admin'],
+            ],
+        ],
+    ];
 
-        $groups = [['id' => 1, 'name' => 'superadmin', 'description' => '']];
+    static function getGroupIdByName($name, $data_groups)
+    {
+        foreach ($data_groups as $group) {
+            if ($group['name'] === $name) {
+                return $group['id'];
+            }
+        }
+        return null;
+    }
 
-        /** Build permissions array as a list of:
-         *
-         * ```
-         * context => [
-         *      [action, description],
-         *      [action, description],
-         *      ...
-         * ]
-         * ```
-         */
-        $permissions = [
-            'users' => [
-                ['name' => 'create', 'description' => 'Create a user'],
-                ['name' => 'list', 'description' => 'List all users'],
-                [
-                    'name' => 'manage_authorizations',
-                    'description' =>
-                        'Add or remove roles/permissions to a user',
-                ],
-                [
-                    'name' => 'manage_bans',
-                    'description' => 'Ban / unban a user',
-                ],
-                [
-                    'name' => 'force_pass_reset',
-                    'description' =>
-                        'Force a user to update his password upon next login',
-                ],
-                [
-                    'name' => 'delete',
-                    'description' =>
-                        'Delete user without removing him from database',
-                ],
-                [
-                    'name' => 'delete_permanently',
-                    'description' =>
-                        'Delete all occurrences of a user from the database',
-                ],
-            ],
-            'podcasts' => [
-                ['name' => 'create', 'description' => 'Add a new podcast'],
-                [
-                    'name' => 'list',
-                    'description' => 'List all podcasts and their episodes',
-                ],
-                ['name' => 'view', 'description' => 'View any podcast'],
-                ['name' => 'edit', 'description' => 'Edit any podcast'],
-                [
-                    'name' => 'manage_contributors',
-                    'description' => 'Add / remove contributors to a podcast',
-                ],
-                [
-                    'name' => 'manage_publication',
-                    'description' => 'Publish / unpublish a podcast',
-                ],
-                [
-                    'name' => 'delete',
-                    'description' =>
-                        'Delete a podcast without removing it from database',
-                ],
-                [
-                    'name' => 'delete_permanently',
-                    'description' => 'Delete any podcast from the database',
-                ],
-            ],
-            'episodes' => [
-                [
-                    'name' => 'list',
-                    'description' => 'List all episodes of any podcast',
-                ],
-                [
-                    'name' => 'create',
-                    'description' => 'Add a new episode to any podcast',
-                ],
-                ['name' => 'edit', 'description' => 'Edit any podcast episode'],
-                [
-                    'name' => 'manage_publications',
-                    'description' => 'Publish / unpublish any podcast episode',
-                ],
-                [
-                    'name' => 'delete',
-                    'description' =>
-                        'Delete any podcast episode without removing it from database',
-                ],
-                [
-                    'name' => 'delete_permanently',
-                    'description' => 'Delete any podcast episode from database',
-                ],
-            ],
-        ];
+    public function run()
+    {
+        $group_id = 0;
+        $data_groups = [];
+        foreach ($this->groups as $group) {
+            array_push($data_groups, [
+                'id' => ++$group_id,
+                'name' => $group['name'],
+                'description' => $group['description'],
+            ]);
+        }
 
         // Map permissions to a format the `auth_permissions` table expects
         $data_permissions = [];
         $data_groups_permissions = [];
         $permission_id = 0;
-        foreach ($permissions as $context => $actions) {
+        foreach ($this->permissions as $context => $actions) {
             foreach ($actions as $action) {
                 array_push($data_permissions, [
                     'id' => ++$permission_id,
-                    'name' => get_permission($context, $action['name']),
+                    'name' => $context . '-' . $action['name'],
                     'description' => $action['description'],
                 ]);
 
-                // add all permissions to superadmin
-                array_push($data_groups_permissions, [
-                    'group_id' => 1,
-                    'permission_id' => $permission_id,
-                ]);
+                foreach ($action['has_permission'] as $role) {
+                    // link permission to specified groups
+                    array_push($data_groups_permissions, [
+                        'group_id' => $this->getGroupIdByName(
+                            $role,
+                            $data_groups
+                        ),
+                        'permission_id' => $permission_id,
+                    ]);
+                }
             }
         }
 
         $this->db->table('auth_permissions')->insertBatch($data_permissions);
-        $this->db->table('auth_groups')->insertBatch($groups);
+        $this->db->table('auth_groups')->insertBatch($data_groups);
         $this->db
             ->table('auth_groups_permissions')
             ->insertBatch($data_groups_permissions);
-
-        // TODO: Remove superadmin user as it is used for testing purposes
-        $this->db->table('users')->insert([
-            'id' => 1,
-            'username' => 'admin',
-            'email' => 'admin@castopod.com',
-            'password_hash' =>
-                // password: AGUehL3P
-                '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
-            'active' => 1,
-        ]);
-        $this->db
-            ->table('auth_groups_users')
-            ->insert(['group_id' => 1, 'user_id' => 1]);
     }
 }
diff --git a/app/Database/Seeds/TestSeeder.php b/app/Database/Seeds/TestSeeder.php
new file mode 100644
index 0000000000000000000000000000000000000000..efdbd5b8d712a4a1b0fc23b8058c83412b650148
--- /dev/null
+++ b/app/Database/Seeds/TestSeeder.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Class TestSeeder
+ * Inserts a superadmin user in the database
+ *
+ * @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 TestSeeder extends Seeder
+{
+    public function run()
+    {
+        /** Inserts an active user with the following credentials:
+         *      username: admin
+         *      password: AGUehL3P
+         */
+        $this->db->table('users')->insert([
+            'id' => 1,
+            'username' => 'admin',
+            'email' => 'admin@castopod.com',
+            'password_hash' =>
+                '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
+            'active' => 1,
+        ]);
+        $this->db
+            ->table('auth_groups_users')
+            ->insert(['group_id' => 1, 'user_id' => 1]);
+    }
+}
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index b4b281ce930ddfc0cbbdb5ed04675436fe04e5e9..a8268f32c6a68eeb782080981d342baf6b28e45b 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -9,7 +9,6 @@ namespace App\Entities;
 
 use App\Models\PodcastModel;
 use CodeIgniter\Entity;
-use League\CommonMark\CommonMarkConverter;
 use Parsedown;
 
 class Episode extends Entity
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 991d1e44304b2d1d9a5b2e4821381c36fbffed17..9a86b1d02f9749adf4283a4c07f6be2752f4cf9e 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -9,7 +9,7 @@ namespace App\Entities;
 
 use App\Models\EpisodeModel;
 use CodeIgniter\Entity;
-use Myth\Auth\Models\UserModel;
+use App\Models\UserModel;
 use Parsedown;
 
 class Podcast extends Entity
@@ -19,7 +19,7 @@ class Podcast extends Entity
     protected string $image_media_path;
     protected string $image_url;
     protected $episodes;
-    protected \Myth\Auth\Entities\User $owner;
+    protected \App\Entities\User $owner;
     protected $contributors;
     protected string $description_html;
 
@@ -110,7 +110,7 @@ class Podcast extends Entity
     /**
      * Returns the podcast owner
      *
-     * @return \Myth\Auth\Entities\User
+     * @return \App\Entities\User
      */
     public function getOwner()
     {
@@ -127,7 +127,7 @@ class Podcast extends Entity
         return $this->owner;
     }
 
-    public function setOwner(\Myth\Auth\Entities\User $user)
+    public function setOwner(\App\Entities\User $user)
     {
         $this->attributes['owner_id'] = $user->id;
 
@@ -137,15 +137,23 @@ class Podcast extends Entity
     /**
      * Returns all podcast contributors
      *
-     * @return \Myth\Auth\Entities\User[]
+     * @return \App\Entities\User[]
      */
     public function getContributors()
     {
-        return (new UserModel())
-            ->select('users.*')
-            ->join('users_podcasts', 'users_podcasts.user_id = users.id')
-            ->where('users_podcasts.podcast_id', $this->attributes['id'])
-            ->findAll();
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcasts must be created before getting contributors.'
+            );
+        }
+
+        if (empty($this->contributors)) {
+            $this->contributors = (new UserModel())->getPodcastContributors(
+                $this->id
+            );
+        }
+
+        return $this->contributors;
     }
 
     public function getDescriptionHtml()
diff --git a/app/Entities/User.php b/app/Entities/User.php
new file mode 100644
index 0000000000000000000000000000000000000000..3277fdcb9edbd01c3ed162b34082950a75810b71
--- /dev/null
+++ b/app/Entities/User.php
@@ -0,0 +1,42 @@
+<?php namespace App\Entities;
+
+use App\Models\PodcastModel;
+
+class User extends \Myth\Auth\Entities\User
+{
+    /**
+     * Per-user podcasts
+     * @var \App\Entities\Podcast[]
+     */
+    protected $podcasts = [];
+
+    /**
+     * Array of field names and the type of value to cast them as
+     * when they are accessed.
+     */
+    protected $casts = [
+        'active' => 'boolean',
+        'force_pass_reset' => 'boolean',
+        'podcast_role' => '?string',
+    ];
+
+    /**
+     * Returns the podcasts the user is contributing to
+     *
+     * @return \App\Entities\Podcast[]
+     */
+    public function getPodcasts()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Users must be created before getting podcasts.'
+            );
+        }
+
+        if (empty($this->podcasts)) {
+            $this->podcasts = (new PodcastModel())->getUserPodcasts($this->id);
+        }
+
+        return $this->podcasts;
+    }
+}
diff --git a/app/Filters/Permission.php b/app/Filters/Permission.php
new file mode 100644
index 0000000000000000000000000000000000000000..a83d966256dc466e266f066d2052d7f3dacc7419
--- /dev/null
+++ b/app/Filters/Permission.php
@@ -0,0 +1,115 @@
+<?php namespace App\Filters;
+
+use App\Models\PodcastModel;
+use Config\Services;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+use CodeIgniter\Filters\FilterInterface;
+use Myth\Auth\Exceptions\PermissionException;
+
+class Permission implements FilterInterface
+{
+    /**
+     * Do whatever processing this filter needs to do.
+     * By default it should not return anything during
+     * normal execution. However, when an abnormal state
+     * is found, it should return an instance of
+     * CodeIgniter\HTTP\Response. If it does, script
+     * execution will end and that Response will be
+     * sent back to the client, allowing for error pages,
+     * redirects, etc.
+     *
+     * @param \CodeIgniter\HTTP\RequestInterface $request
+     * @param array|null                         $params
+     *
+     * @return mixed
+     */
+    public function before(RequestInterface $request, $params = null)
+    {
+        if (!function_exists('logged_in')) {
+            helper('auth');
+        }
+
+        if (empty($params)) {
+            return;
+        }
+
+        $authenticate = Services::authentication();
+
+        // if no user is logged in then send to the login form
+        if (!$authenticate->check()) {
+            session()->set('redirect_url', current_url());
+            return redirect('login');
+        }
+
+        helper('misc');
+        $authorize = Services::authorization();
+        $router = Services::router();
+        $routerParams = $router->params();
+        $result = false;
+
+        // Check if user has at least one of the permissions
+        foreach ($params as $permission) {
+            // check if permission is for a specific podcast
+            if (
+                (startsWith($permission, 'podcast-') ||
+                    startsWith($permission, 'podcast_episodes-')) &&
+                count($routerParams) > 0
+            ) {
+                if (
+                    $group_id = (new PodcastModel())->getContributorGroupId(
+                        $authenticate->id(),
+                        $routerParams[0]
+                    )
+                ) {
+                    if (
+                        $authorize->groupHasPermission($permission, $group_id)
+                    ) {
+                        $result = true;
+                        break;
+                    }
+                }
+            } elseif (
+                $authorize->hasPermission($permission, $authenticate->id())
+            ) {
+                $result = true;
+                break;
+            }
+        }
+
+        if (!$result) {
+            if ($authenticate->silent()) {
+                $redirectURL = session('redirect_url') ?? '/';
+                unset($_SESSION['redirect_url']);
+                return redirect()
+                    ->to($redirectURL)
+                    ->with('error', lang('Auth.notEnoughPrivilege'));
+            } else {
+                throw new PermissionException(lang('Auth.notEnoughPrivilege'));
+            }
+        }
+    }
+
+    //--------------------------------------------------------------------
+
+    /**
+     * Allows After filters to inspect and modify the response
+     * object as needed. This method does not allow any way
+     * to stop execution of other after filters, short of
+     * throwing an Exception or Error.
+     *
+     * @param \CodeIgniter\HTTP\RequestInterface  $request
+     * @param \CodeIgniter\HTTP\ResponseInterface $response
+     * @param array|null                          $arguments
+     *
+     * @return void
+     */
+    public function after(
+        RequestInterface $request,
+        ResponseInterface $response,
+        $arguments = null
+    ) {
+    }
+
+    //--------------------------------------------------------------------
+}
diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php
deleted file mode 100644
index 344e0636a2a8bd632996c8a080ee4670e5c5c1c1..0000000000000000000000000000000000000000
--- a/app/Helpers/auth_helper.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-/**
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-/**
- * Gets the permission name by concatenating the context and action
- *
- * @param string $context
- * @param string $action
- *
- * @return string permission name
- */
-function get_permission($context, $action)
-{
-    return $context . '-' . $action;
-}
diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php
index 804e5a4976a741c75e022150afdd0de6cd3e14ad..ce7af19a5f8dcec266d43e00e332d7333c7e77bf 100644
--- a/app/Helpers/misc_helper.php
+++ b/app/Helpers/misc_helper.php
@@ -21,3 +21,16 @@ function get_browser_language($http_accept_language)
 
     return null;
 }
+
+/**
+ * Check if a string starts with some characters
+ *
+ * @param string $string
+ * @param string $query
+ *
+ * @return bool
+ */
+function startsWith($string, $query)
+{
+    return substr($string, 0, strlen($query)) === $query;
+}
diff --git a/app/Language/en/AdminNavigation.php b/app/Language/en/AdminNavigation.php
index b3a390b9b3ba56cb498fa7300935d584e0f9cddf..61c7db3feca1a5c9abcd24b1f05313ec56a87f28 100644
--- a/app/Language/en/AdminNavigation.php
+++ b/app/Language/en/AdminNavigation.php
@@ -10,6 +10,7 @@
      'podcasts' => 'Podcasts',
      'users' => 'Users',
      'admin_home' => 'Home',
+     'my_podcasts' => 'My podcasts',
      'podcast_list' => 'All podcasts',
      'podcast_create' => 'New podcast',
      'user_list' => 'All users',
diff --git a/app/Language/en/Contributor.php b/app/Language/en/Contributor.php
index 552c9f64fc946619b13bde5979b1c6f3df8f9027..57a8448a10ea10b5c001c9cd632a452311deb81a 100644
--- a/app/Language/en/Contributor.php
+++ b/app/Language/en/Contributor.php
@@ -6,6 +6,9 @@
  */
 
 return [
+    'removeOwnerContributorError' => 'You can\'t remove the podcast owner!',
+    'removeContributorSuccess' => 'You have successfully removed {username} from {podcastTitle}',
+    'alreadyAddedError' => 'The contributor you\'re trying to add has already been added!',
     'podcast_contributors' => 'Podcast contributors',
     'add' => 'Add contributor',
     'add_contributor' => 'Add a contributor for {0}',
diff --git a/app/Language/en/User.php b/app/Language/en/User.php
index c9e0fc03ccd8d15f37375a507c4b53acb1c4f7f4..aa5491c0a48f0176858ba868096b2112cd856305 100644
--- a/app/Language/en/User.php
+++ b/app/Language/en/User.php
@@ -6,11 +6,15 @@
  */
 
 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.',
-    'deleteSuccess' => 'User has been deleted.',
+    'createSuccess' => 'User created successfully! {username} will be prompted with a password reset upon first authentication.',
+    'rolesEditSuccess' => '{username}\'s roles have been successfully updated.',
+    'forcePassResetSuccess' => '{username} will be prompted with a password reset upon next visit.',
+    'banSuccess' => '{username} has been banned.',
+    'unbanSuccess' => '{username} has been unbanned.',
+    'banSuperAdminError' => '{username} is a superadmin, one does not simply ban a superadmin…',
+    'deleteSuperAdminError' => '{username} is a superadmin, one does not simply delete a superadmin…',
+    'deleteSuccess' => '{username} has been deleted.',
+    'edit_roles' => 'Edit {username}\'s roles',
     'forcePassReset' => 'Force pass reset',
     'ban' => 'Ban',
     'unban' => 'Unban',
@@ -24,6 +28,7 @@ return [
         'new_password' => 'New Password',
         'repeat_password' => 'Repeat password',
         'repeat_new_password' => 'Repeat new password',
+        'roles' => 'Roles',
         'submit_create' => 'Create user',
         'submit_edit' => 'Save',
     ]
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 86d47c3a7b68f685d08feaf48ec1641eeff3cad0..ece640d99337c6f871e6236519e4d400015258d2 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -8,8 +8,6 @@
 namespace App\Models;
 
 use CodeIgniter\Model;
-use Myth\Auth\Authorization\GroupModel;
-use Myth\Auth\Config\Services;
 
 class PodcastModel extends Model
 {
@@ -58,7 +56,7 @@ class PodcastModel extends Model
     ];
     protected $validationMessages = [];
 
-    protected $afterInsert = ['clearCache', 'createPodcastPermissions'];
+    protected $afterInsert = ['clearCache'];
     protected $afterUpdate = ['clearCache'];
     protected $beforeDelete = ['clearCache'];
 
@@ -77,17 +75,29 @@ class PodcastModel extends Model
             ->findAll();
     }
 
-    public function addContributorToPodcast($user_id, $podcast_id)
+    public function addPodcastContributor($user_id, $podcast_id, $group_id)
     {
         $data = [
             'user_id' => (int) $user_id,
             'podcast_id' => (int) $podcast_id,
+            'group_id' => (int) $group_id,
         ];
 
         return $this->db->table('users_podcasts')->insert($data);
     }
 
-    public function removeContributorFromPodcast($user_id, $podcast_id)
+    public function updatePodcastContributor($user_id, $podcast_id, $group_id)
+    {
+        return $this->db
+            ->table('users_podcasts')
+            ->where([
+                'user_id' => (int) $user_id,
+                'podcast_id' => (int) $podcast_id,
+            ])
+            ->update(['group_id' => $group_id]);
+    }
+
+    public function removePodcastContributor($user_id, $podcast_id)
     {
         return $this->db
             ->table('users_podcasts')
@@ -98,6 +108,24 @@ class PodcastModel extends Model
             ->delete();
     }
 
+    public function getContributorGroupId($user_id, $podcast_id)
+    {
+        // TODO: return only the group id
+        $user_podcast = $this->db
+            ->table('users_podcasts')
+            ->select('group_id')
+            ->where([
+                'user_id' => $user_id,
+                'podcast_id' => $podcast_id,
+            ])
+            ->get()
+            ->getResultObject();
+
+        return (int) count($user_podcast) > 0
+            ? $user_podcast[0]->group_id
+            : false;
+    }
+
     protected function clearCache(array $data)
     {
         $podcast = $this->find(
@@ -109,101 +137,11 @@ class PodcastModel extends Model
         cache()->delete(md5($podcast->link));
         // TODO: clear cache for every podcast's episode page?
         // foreach ($podcast->episodes as $episode) {
-        //     $cache->delete(md5($episode->link));
+        //     cache()->delete(md5($episode->link));
         // }
 
         $data['podcast'] = $podcast;
 
         return $data;
     }
-
-    protected function createPodcastPermissions(array $data)
-    {
-        $authorize = Services::authorization();
-
-        $podcast = $data['podcast'];
-
-        $podcast_permissions = [
-            'podcasts:' . $podcast->id => [
-                [
-                    'name' => 'View',
-                    'description' => "View the $podcast->name podcast",
-                ],
-                [
-                    'name' => 'edit',
-                    'description' => "Edit the $podcast->name podcast",
-                ],
-                [
-                    'name' => 'delete',
-                    'description' => "Delete the $podcast->name podcast without removing it from the database",
-                ],
-                [
-                    'name' => 'delete_permanently',
-                    'description' => "Delete the $podcast->name podcast from the database",
-                ],
-                [
-                    'name' => 'manage_contributors',
-                    'description' => "Add / remove contributors to the $podcast->name podcast and edit their roles",
-                ],
-                [
-                    'name' => 'manage_publication',
-                    'description' => "Publish / unpublish $podcast->name",
-                ],
-            ],
-            'podcasts:' . $podcast->id . ':episodes' => [
-                [
-                    'name' => 'list',
-                    'description' => "List all episodes of the $podcast->name podcast",
-                ],
-                [
-                    'name' => 'create',
-                    'description' => "Add new episodes for the $podcast->name podcast",
-                ],
-                [
-                    'name' => 'edit',
-                    'description' => "Edit an episode of the $podcast->name podcast",
-                ],
-                [
-                    'name' => 'delete',
-                    'description' => "Delete an episode of the $podcast->name podcast without removing it from the database",
-                ],
-                [
-                    'name' => 'delete_permanently',
-                    'description' => "Delete all occurrences of an episode of the $podcast->name podcast from the database",
-                ],
-                [
-                    'name' => 'manage_publications',
-                    'description' => "Publish / unpublish episodes of the $podcast->name podcast",
-                ],
-            ],
-        ];
-
-        $group_model = new GroupModel();
-        $owner_group_id = $group_model->insert(
-            [
-                'name' => "podcasts:$podcast->id" . '_owner',
-                'description' => "The owner of the $podcast->name podcast",
-            ],
-            true
-        );
-
-        // add podcast owner to owner group
-        $authorize->addUserToGroup($podcast->owner_id, $owner_group_id);
-
-        foreach ($podcast_permissions as $context => $actions) {
-            foreach ($actions as $action) {
-                $permission_id = $authorize->createPermission(
-                    get_permission($context, $action['name']),
-                    $action['description']
-                );
-
-                $authorize->addPermissionToGroup(
-                    $permission_id,
-                    $owner_group_id
-                );
-            }
-        }
-
-        return $data;
-    }
 }
diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..876cc94712362fd0db2f3f2a6b127f93b6986a8c
--- /dev/null
+++ b/app/Models/UserModel.php
@@ -0,0 +1,28 @@
+<?php namespace App\Models;
+
+use App\Entities\User;
+
+class UserModel extends \Myth\Auth\Models\UserModel
+{
+    protected $returnType = User::class;
+
+    public function getPodcastContributors($podcast_id)
+    {
+        return $this->select('users.*, auth_groups.name as podcast_role')
+            ->join('users_podcasts', 'users_podcasts.user_id = users.id')
+            ->join('auth_groups', 'auth_groups.id = users_podcasts.group_id')
+            ->where('users_podcasts.podcast_id', $podcast_id)
+            ->findAll();
+    }
+
+    public function getPodcastContributor($user_id, $podcast_id)
+    {
+        return $this->select('users.*')
+            ->join('users_podcasts', 'users_podcasts.user_id = users.id')
+            ->where([
+                'users.id' => $user_id,
+                'podcast_id' => $podcast_id,
+            ])
+            ->first();
+    }
+}
diff --git a/app/Views/admin/_partials/_podcast-card.php b/app/Views/admin/_partials/_podcast-card.php
index 4f769df73ce0381dad7d49d582e006ece2e6010e..a56c7fe6a619971e8b4799adc71e3ebb6a0b2187 100644
--- a/app/Views/admin/_partials/_podcast-card.php
+++ b/app/Views/admin/_partials/_podcast-card.php
@@ -16,7 +16,7 @@
         ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
     'Podcast.edit'
 ) ?>"><?= icon('edit') ?></a>
-        <a class="inline-flex p-2 bg-gray-100 rounded-full shadow-xs text-teal-gray hover:bg-gray-200" href="<?= route_to(
+        <a class="inline-flex p-2 text-gray-700 bg-gray-100 rounded-full shadow-xs hover:bg-gray-200" href="<?= route_to(
             'podcast_view',
             $podcast->id
         ) ?>" data-toggle="tooltip" data-placement="bottom" title="<?= lang(
diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php
index 95fca996afc0203d2a75a03cefcf5475739dfe89..5698f1055bcc4221c191eea0afdddaefe3a8954b 100644
--- a/app/Views/admin/_sidenav.php
+++ b/app/Views/admin/_sidenav.php
@@ -3,7 +3,7 @@ $navigation = [
     'dashboard' => ['icon' => 'dashboard', 'items' => ['admin_home']],
     'podcasts' => [
         'icon' => 'mic',
-        'items' => ['podcast_list', 'podcast_create'],
+        'items' => ['my_podcasts', 'podcast_list', 'podcast_create'],
     ],
     'users' => ['icon' => 'group', 'items' => ['user_list', 'user_create']],
 ]; ?>
diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php
index 3c6db65000c08c87f19d1213fbaf47bf66b04c40..f80732a2cf36c5fb89588ff590f6f6fa1fb0b322 100644
--- a/app/Views/admin/contributor/list.php
+++ b/app/Views/admin/contributor/list.php
@@ -19,7 +19,7 @@
     <thead>
         <tr>
             <th class="px-4 py-2">Username</th>
-            <th class="px-4 py-2">Permissions</th>
+            <th class="px-4 py-2">Role</th>
             <th class="px-4 py-2">Actions</th>
         </tr>
     </thead>
@@ -27,10 +27,7 @@
         <?php foreach ($podcast->contributors as $contributor): ?>
         <tr>
             <td class="px-4 py-2 border"><?= $contributor->username ?></td>
-            <td class="px-4 py-2 border">[<?= implode(
-                ', ',
-                $contributor->permissions
-            ) ?>]</td>
+            <td class="px-4 py-2 border"><?= $contributor->podcast_role ?></td>
             <td class="px-4 py-2 border">
                 <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
                     'contributor_edit',
diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php
index 612b8d7130ca024a21defc30b6205f32de2dca03..0238c0fa77bd9d18b9bab0f676690334be8e5c4e 100644
--- a/app/Views/admin/episode/create.php
+++ b/app/Views/admin/episode/create.php
@@ -37,8 +37,6 @@
     <textarea class="hidden form-textarea" id="description" name="description" required data-editor="markdown"><?= old(
         'description'
     ) ?></textarea>
-    <button type="button" data-editor-view="markdown">Markdown</button>
-    <button type="button" data-editor-view="wysiwyg">WYSIWYG</button>
 </div>
 
 <div class="flex flex-col mb-4">
diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php
index 678328964a15bce780e3fc387e626a6f268317f8..2f4fa21aa9e15337644de25dca8ac203c76a1787 100644
--- a/app/Views/admin/podcast/create.php
+++ b/app/Views/admin/podcast/create.php
@@ -162,7 +162,9 @@
     <label for="custom_html_head"><?= esc(
         lang('Podcast.form.custom_html_head')
     ) ?></label>
-    <textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"></textarea>
+    <textarea class="form-textarea" id="custom_html_head" name="custom_html_head" data-editor="html"><?= old(
+        'custom_html_head'
+    ) ?></textarea>
 </div>
 
 <button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
diff --git a/app/Views/admin/user/edit.php b/app/Views/admin/user/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc3594690260473535d5e32be266df832d0c5c4f
--- /dev/null
+++ b/app/Views/admin/user/edit.php
@@ -0,0 +1,33 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('User.edit_roles', ['username' => $user->username]) ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+
+<form action="<?= route_to(
+    'user_edit',
+    $user->id
+) ?>" method="post" class="flex flex-col max-w-lg">
+    <?= csrf_field() ?>
+
+    <label for="roles"><?= lang('User.form.roles') ?></label>
+    <select id="roles" name="roles[]" autocomplete="off" class="mb-6 form-multiselect" multiple>
+        <?php foreach ($roles as $role): ?>
+            <option value="<?= $role->id ?>"
+            <?php if (
+                in_array($role->name, $user->roles)
+            ): ?> selected <?php endif; ?>>
+                <?= $role->name ?>
+            </option>
+        <?php endforeach; ?>
+    </select>
+
+    <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/user/list.php b/app/Views/admin/user/list.php
index b41502250d700da89d6408f6f6e0e4702b9027e2..cd64b0761519bb1c5981910f434ae5c21a4c0e5a 100644
--- a/app/Views/admin/user/list.php
+++ b/app/Views/admin/user/list.php
@@ -1,3 +1,5 @@
+<?php helper('html'); ?>
+
 <?= $this->extend('admin/_layout') ?>
 
 <?= $this->section('title') ?>
@@ -12,7 +14,7 @@
         <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">Roles</th>
             <th class="px-4 py-2">Banned?</th>
             <th class="px-4 py-2">Actions</th>
         </tr>
@@ -22,15 +24,23 @@
         <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">
+                [<?= implode(', ', $user->roles) ?>]
+                <a class="inline-flex p-2 mr-2 text-teal-700 bg-teal-100 rounded-full shadow-xs hover:bg-teal-200" href="<?= route_to(
+                    'user_edit',
+                    $user->id
+                ) ?>" data-toggle="tooltip" data-placement="bottom"
+                title="<?= lang('User.edit_roles', [
+                    'username' => $user->username,
+                ]) ?>">
+                <?= icon('edit') ?>
+                </a>
+            </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(
+                <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
                     'user_force_pass_reset',
                     $user->id
                 ) ?>"><?= lang('User.forcePassReset') ?></a>
diff --git a/app/Views/auth/change_password.php b/app/Views/auth/change_password.php
deleted file mode 100644
index 3ca545b6e7e890c7981cb4a793c807ff42947d04..0000000000000000000000000000000000000000
--- a/app/Views/auth/change_password.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?= $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/composer.json b/composer.json
index 1a3749eff411b82a3ced628ec425ade0cdf74457..ae4625de3faf1e6c2419ee5836d5eb99007b7808 100644
--- a/composer.json
+++ b/composer.json
@@ -6,12 +6,12 @@
   "license": "AGPL-3.0-or-later",
   "require": {
     "php": ">=7.2",
-    "codeigniter4/framework": "4.0.3",
     "james-heinrich/getid3": "~2.0.0-dev",
     "whichbrowser/parser": "^2.0",
     "geoip2/geoip2": "~2.0",
-    "myth/auth": "1.0-beta.2",
-    "erusev/parsedown": "^1.7"
+    "myth/auth": "dev-develop",
+    "erusev/parsedown": "^1.7",
+    "codeigniter4/codeigniter4": "dev-develop"
   },
   "require-dev": {
     "mikey179/vfsstream": "1.6.*",
@@ -32,5 +32,13 @@
     "forum": "http://forum.codeigniter.com/",
     "source": "https://github.com/codeigniter4/CodeIgniter4",
     "slack": "https://codeigniterchat.slack.com"
-  }
+  },
+  "minimum-stability": "dev",
+  "prefer-stable": true,
+  "repositories": [
+    {
+      "type": "vcs",
+      "url": "https://github.com/codeigniter4/codeigniter4"
+    }
+  ]
 }
diff --git a/composer.lock b/composer.lock
index 752e638cc2c9e3aa31e93b2e9ea879f3bc558ca9..4b63d07ee11d324a3d9a39eda9af59a2185dd7f9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,20 +4,20 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "3656eaed72238d7b46af985ec86f6533",
+    "content-hash": "b483083efa09cc772800d5df7d339d02",
     "packages": [
         {
-            "name": "codeigniter4/framework",
-            "version": "v4.0.3",
+            "name": "codeigniter4/codeigniter4",
+            "version": "dev-develop",
             "source": {
                 "type": "git",
-                "url": "https://github.com/codeigniter4/framework.git",
-                "reference": "edd88b18483e309bab1411651d846aace255ab36"
+                "url": "https://github.com/codeigniter4/CodeIgniter4.git",
+                "reference": "81c08a5ddd70d2243c0842dfcc22c93dd044bc42"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/codeigniter4/framework/zipball/edd88b18483e309bab1411651d846aace255ab36",
-                "reference": "edd88b18483e309bab1411651d846aace255ab36",
+                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/81c08a5ddd70d2243c0842dfcc22c93dd044bc42",
+                "reference": "81c08a5ddd70d2243c0842dfcc22c93dd044bc42",
                 "shasum": ""
             },
             "require": {
@@ -32,8 +32,10 @@
             },
             "require-dev": {
                 "codeigniter4/codeigniter4-standard": "^1.0",
+                "fzaninotto/faker": "^1.9@dev",
                 "mikey179/vfsstream": "1.6.*",
                 "phpunit/phpunit": "^8.5",
+                "predis/predis": "^1.1",
                 "squizlabs/php_codesniffer": "^3.3"
             },
             "type": "project",
@@ -42,13 +44,28 @@
                     "CodeIgniter\\": "system/"
                 }
             },
-            "notification-url": "https://packagist.org/downloads/",
+            "scripts": {
+                "post-update-cmd": [
+                    "@composer dump-autoload",
+                    "CodeIgniter\\ComposerScripts::postUpdate",
+                    "bash admin/setup.sh"
+                ],
+                "test": [
+                    "phpunit"
+                ]
+            },
             "license": [
                 "MIT"
             ],
             "description": "The CodeIgniter framework v4",
             "homepage": "https://codeigniter.com",
-            "time": "2020-05-01T05:01:20+00:00"
+            "support": {
+                "forum": "http://forum.codeigniter.com/",
+                "source": "https://github.com/codeigniter4/CodeIgniter4",
+                "slack": "https://codeigniterchat.slack.com",
+                "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
+            },
+            "time": "2020-07-29T03:07:26+00:00"
         },
         {
             "name": "composer/ca-bundle",
@@ -217,16 +234,16 @@
         },
         {
             "name": "james-heinrich/getid3",
-            "version": "2.0.x-dev",
+            "version": "v2.0.0-beta3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/JamesHeinrich/getID3.git",
-                "reference": "8cf765ec4c42ed732993a9aa60b638ee398df154"
+                "reference": "5515a2d24667c3c0ff49fdcbdadc405c0880c7a2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/8cf765ec4c42ed732993a9aa60b638ee398df154",
-                "reference": "8cf765ec4c42ed732993a9aa60b638ee398df154",
+                "url": "https://api.github.com/repos/JamesHeinrich/getID3/zipball/5515a2d24667c3c0ff49fdcbdadc405c0880c7a2",
+                "reference": "5515a2d24667c3c0ff49fdcbdadc405c0880c7a2",
                 "shasum": ""
             },
             "require": {
@@ -294,7 +311,7 @@
                 "tags",
                 "video"
             ],
-            "time": "2019-07-22T12:33:16+00:00"
+            "time": "2020-07-21T08:15:44+00:00"
         },
         {
             "name": "kint-php/kint",
@@ -581,16 +598,16 @@
         },
         {
             "name": "myth/auth",
-            "version": "1.0-beta.2",
+            "version": "dev-develop",
             "source": {
                 "type": "git",
                 "url": "https://github.com/lonnieezell/myth-auth.git",
-                "reference": "b110088785ba22a82264e1df444621f3e1618f95"
+                "reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/b110088785ba22a82264e1df444621f3e1618f95",
-                "reference": "b110088785ba22a82264e1df444621f3e1618f95",
+                "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694",
+                "reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694",
                 "shasum": ""
             },
             "require": {
@@ -600,7 +617,7 @@
                 "codeigniter4/codeigniter4": "dev-develop",
                 "fzaninotto/faker": "^1.9@dev",
                 "mockery/mockery": "^1.0",
-                "phpunit/phpunit": "^7.0"
+                "phpunit/phpunit": "8.5.*"
             },
             "type": "library",
             "autoload": {
@@ -627,7 +644,17 @@
                 "authorization",
                 "codeigniter"
             ],
-            "time": "2019-12-12T05:12:25+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/lonnieezell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/lonnieezell",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2020-07-16T14:00:14+00:00"
         },
         {
             "name": "psr/cache",
@@ -1106,28 +1133,27 @@
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "5.1.0",
+            "version": "5.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e"
+                "reference": "3170448f5769fe19f456173d833734e0ff1b84df"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e",
-                "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df",
+                "reference": "3170448f5769fe19f456173d833734e0ff1b84df",
                 "shasum": ""
             },
             "require": {
-                "ext-filter": "^7.1",
-                "php": "^7.2",
-                "phpdocumentor/reflection-common": "^2.0",
-                "phpdocumentor/type-resolver": "^1.0",
-                "webmozart/assert": "^1"
+                "ext-filter": "*",
+                "php": "^7.2 || ^8.0",
+                "phpdocumentor/reflection-common": "^2.2",
+                "phpdocumentor/type-resolver": "^1.3",
+                "webmozart/assert": "^1.9.1"
             },
             "require-dev": {
-                "doctrine/instantiator": "^1",
-                "mockery/mockery": "^1"
+                "mockery/mockery": "~1.3.2"
             },
             "type": "library",
             "extra": {
@@ -1155,7 +1181,7 @@
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2020-02-22T12:28:44+00:00"
+            "time": "2020-07-20T20:05:34+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
@@ -2398,12 +2424,13 @@
         }
     ],
     "aliases": [],
-    "minimum-stability": "stable",
+    "minimum-stability": "dev",
     "stability-flags": {
         "james-heinrich/getid3": 20,
-        "myth/auth": 10
+        "myth/auth": 20,
+        "codeigniter4/codeigniter4": 20
     },
-    "prefer-stable": false,
+    "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
         "php": ">=7.2"
diff --git a/docs/setup-development.md b/docs/setup-development.md
index 7991789ab5da37592e029f78d0e3d6c09ebcab3a..47668c9aef34075c353d71b8fc2bce6c0bc824b9 100644
--- a/docs/setup-development.md
+++ b/docs/setup-development.md
@@ -97,21 +97,34 @@ docker ps -a
 
 ## Initialize and populate database
 
-Build the database with the migrate command:
+1. Build the database with the migrate command:
 
 ```bash
 # loads the database schema during first migration
 docker-compose run --rm app php spark migrate -all
 ```
 
-Populate the database with the required data:
+2. Populate the database with the required data:
 
 ```bash
 # Populates all categories
 docker-compose run --rm app php spark db:seed CategorySeeder
 docker-compose run --rm app php spark db:seed LanguageSeeder
+docker-compose run --rm app php spark db:seed PlatformSeeder
+docker-compose run --rm app php spark db:seed AuthSeeder
 ```
 
+3. (optionnal) Populate the database with test data:
+
+```bash
+docker-compose run --rm app php spark db:seed TestSeeder
+```
+
+This will add an active superadmin user with the following credentials:
+
+- username: **admin**
+- password: **AGUehL3P**
+
 ## Install/Update app dependencies
 
 Castopod uses `composer` to manage php dependencies and `npm` to manage javascript dependencies.
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 96947df5a615f87e28d550ba7b41b94abd39cc71..80664cbbfefb8e6a5f6079ebe6b002b1ece22bb0 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<phpunit bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
+<phpunit bootstrap="vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php"
 		backupGlobals="false"
 		colors="true"
 		convertErrorsToExceptions="true"