diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 494a3f62c086dde7a343f42ee5767ac4d4c6d080..640d7250d0e1b30ae221dc6461e0f491a66ec86b 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -36,11 +36,12 @@ $routes->addPlaceholder('username', '[a-zA-Z0-9 ]{3,}');
 // route since we don't have to scan directories.
 $routes->get('/', 'Home::index', ['as' => 'home']);
 
+// Public routes
 $routes->group('@(:podcastName)', function ($routes) {
-    $routes->add('/', 'Podcast/$1', ['as' => 'podcast']);
+    $routes->get('/', 'Podcast/$1', ['as' => 'podcast']);
 
-    $routes->add('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
-    $routes->add('episodes/(:episodeSlug)', 'Episode/$1/$2', [
+    $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']);
+    $routes->get('episodes/(:episodeSlug)', 'Episode/$1/$2', [
         'as' => 'episode',
     ]);
 });
@@ -51,74 +52,119 @@ $routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
 ]);
 
 // Show the Unknown UserAgents
-$routes->add('.well-known/unknown-useragents', 'UnknownUserAgents');
-$routes->add('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
+$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
+$routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
 
 // Admin area
 $routes->group(
     config('App')->adminGateway,
     ['namespace' => 'App\Controllers\Admin'],
     function ($routes) {
-        $routes->add('/', 'Home', [
+        $routes->get('/', 'Home', [
             'as' => 'admin',
         ]);
 
-        $routes->add('new-podcast', 'Podcast::create', [
+        $routes->get('my-podcasts', 'Podcast::myPodcasts', [
+            'as' => 'my_podcasts',
+        ]);
+        $routes->get('podcasts', 'Podcast::list', [
+            'as' => 'podcast_list',
+            'filter' => 'permission:podcasts-list',
+        ]);
+        $routes->get('new-podcast', 'Podcast::create', [
             'as' => 'podcast_create',
+            'filter' => 'permission:podcasts-create',
+        ]);
+        $routes->post('new-podcast', 'Podcast::attemptCreate', [
+            'filter' => 'permission:podcasts-create',
         ]);
-        $routes->add('podcasts', 'Podcast::list', ['as' => 'podcast_list']);
 
-        $routes->group('podcasts/@(:podcastName)', function ($routes) {
-            $routes->add('edit', 'Podcast::edit/$1', [
+        // Use ids in admin area to help permission and group lookups
+        $routes->group('podcasts/(:num)', function ($routes) {
+            $routes->get('edit', 'Podcast::edit/$1', [
                 'as' => 'podcast_edit',
             ]);
+            $routes->post('edit', 'Podcast::attemptEdit/$1');
             $routes->add('delete', 'Podcast::delete/$1', [
                 'as' => 'podcast_delete',
             ]);
 
-            $routes->add('new-episode', 'Episode::create/$1', [
+            // Podcast episodes
+            $routes->get('episodes', 'Episode::list/$1', [
+                'as' => 'episode_list',
+            ]);
+            $routes->get('new-episode', 'Episode::create/$1', [
                 'as' => 'episode_create',
             ]);
-            $routes->add('episodes', 'Episode::list/$1', [
-                'as' => 'episode_list',
+            $routes->post('new-episode', 'Episode::attemptCreate/$1');
+
+            $routes->get('episodes/(:num)/edit', 'Episode::edit/$1/$2', [
+                'as' => 'episode_edit',
+            ]);
+            $routes->post('episodes/(:num)/edit', 'Episode::attemptEdit/$1/$2');
+            $routes->add('episodes/(:num)/delete', 'Episode::delete/$1/$2', [
+                'as' => 'episode_delete',
             ]);
 
-            $routes->add(
-                'episodes/(:episodeSlug)/edit',
-                'Episode::edit/$1/$2',
+            // Podcast contributors
+            $routes->get('contributors', 'Contributor::list/$1', [
+                'as' => 'contributor_list',
+            ]);
+            $routes->get('add-contributor', 'Contributor::add/$1', [
+                'as' => 'contributor_add',
+            ]);
+            $routes->post('add-contributor', 'Contributor::attemptAdd/$1');
+            $routes->get(
+                'contributors/(:num)/edit',
+                'Contributor::edit/$1/$2',
                 [
-                    'as' => 'episode_edit',
+                    'as' => 'contributor_edit',
                 ]
             );
+            $routes->post(
+                'contributors/(:num)/edit',
+                'Contributor::attemptEdit/$1/$2'
+            );
             $routes->add(
-                'episodes/(:episodeSlug)/delete',
-                'Episode::delete/$1/$2',
-                [
-                    'as' => 'episode_delete',
-                ]
+                'contributors/(:num)/remove',
+                'Contributor::remove/$1/$2',
+                ['as' => 'contributor_remove']
             );
         });
 
         // Users
-        $routes->add('users', 'User::list', ['as' => 'user_list']);
-        $routes->add('new-user', 'User::create', ['as' => 'user_create']);
+        $routes->get('users', 'User::list', [
+            'as' => 'user_list',
+            'filter' => 'permission:users-list',
+        ]);
+        $routes->get('new-user', 'User::create', [
+            'as' => 'user_create',
+            'filter' => 'permission:users-create',
+        ]);
+        $routes->post('new-user', 'User::attemptCreate', [
+            'filter' => 'permission:users-create',
+        ]);
 
-        $routes->add('users/@(:any)/ban', 'User::ban/$1', [
+        $routes->add('users/(:num)/ban', 'User::ban/$1', [
             'as' => 'user_ban',
+            'filter' => 'permission:users-manage_bans',
         ]);
-        $routes->add('users/@(:any)/unban', 'User::unBan/$1', [
+        $routes->add('users/(:num)/unban', 'User::unBan/$1', [
             'as' => 'user_unban',
+            'filter' => 'permission:users-manage_bans',
         ]);
         $routes->add(
-            'users/@(:any)/force-pass-reset',
+            'users/(:num)/force-pass-reset',
             'User::forcePassReset/$1',
             [
                 'as' => 'user_force_pass_reset',
+                'filter' => 'permission:users-force_pass_reset',
             ]
         );
 
-        $routes->add('users/@(:any)/delete', 'User::delete/$1', [
+        $routes->add('users/(:num)/delete', 'User::delete/$1', [
             'as' => 'user_delete',
+            'filter' => 'permission:users-delete',
         ]);
 
         // My account
diff --git a/app/Controllers/Admin/Contributor.php b/app/Controllers/Admin/Contributor.php
new file mode 100644
index 0000000000000000000000000000000000000000..1cf39aee2ae21de24575e26115c2b68634b1e64b
--- /dev/null
+++ b/app/Controllers/Admin/Contributor.php
@@ -0,0 +1,187 @@
+<?php
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers\Admin;
+
+use App\Models\PodcastModel;
+use Myth\Auth\Authorization\GroupModel;
+use Myth\Auth\Config\Services;
+use Myth\Auth\Models\UserModel;
+
+class Contributor extends BaseController
+{
+    protected \App\Entities\Podcast $podcast;
+    protected ?\Myth\Auth\Entities\User $user;
+
+    public function _remap($method, ...$params)
+    {
+        if (
+            !has_permission('podcasts-manage_contributors') ||
+            !has_permission("podcasts:$params[0]-manage_contributors")
+        ) {
+            throw new \RuntimeException(lang('Auth.notEnoughPrivilege'));
+        }
+
+        $podcast_model = new PodcastModel();
+
+        $this->podcast = $podcast_model->find($params[0]);
+
+        if (count($params) > 1) {
+            $user_model = new UserModel();
+            if (
+                !($this->user = $user_model
+                    ->select('users.*')
+                    ->join(
+                        'users_podcasts',
+                        'users_podcasts.user_id = users.id'
+                    )
+                    ->where([
+                        'users.id' => $params[1],
+                        'podcast_id' => $params[0],
+                    ])
+                    ->first())
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+        }
+
+        return $this->$method();
+    }
+
+    public function list()
+    {
+        $data = [
+            'podcast' => $this->podcast,
+        ];
+
+        echo view('admin/contributor/list', $data);
+    }
+
+    public function add()
+    {
+        $user_model = new UserModel();
+        $group_model = new GroupModel();
+
+        $roles = $group_model
+            ->select('auth_groups.*')
+            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
+            ->findAll();
+
+        $data = [
+            'podcast' => $this->podcast,
+            'users' => $user_model->findAll(),
+            'roles' => $roles,
+        ];
+
+        echo view('admin/contributor/add', $data);
+    }
+
+    public function attemptAdd()
+    {
+        $authorize = Services::authorization();
+
+        $user_id = (int) $this->request->getPost('user');
+        $group_id = (int) $this->request->getPost('role');
+
+        // Add user to chosen group
+        $authorize->addUserToGroup($user_id, $group_id);
+
+        (new PodcastModel())->addContributorToPodcast(
+            $user_id,
+            $this->podcast->id
+        );
+
+        return redirect()->route('contributor_list', [$this->podcast->id]);
+    }
+
+    public function edit()
+    {
+        $group_model = new GroupModel();
+
+        $roles = $group_model
+            ->select('auth_groups.*')
+            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
+            ->findAll();
+
+        $user_role = $group_model
+            ->select('auth_groups.*')
+            ->join(
+                'auth_groups_users',
+                'auth_groups_users.group_id = auth_groups.id'
+            )
+            ->where('auth_groups_users.user_id', $this->user->id)
+            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
+            ->first();
+
+        $data = [
+            'podcast' => $this->podcast,
+            'user' => $this->user,
+            'user_role' => $user_role,
+            'roles' => $roles,
+        ];
+
+        echo view('admin/contributor/edit', $data);
+    }
+
+    public function attemptEdit()
+    {
+        $authorize = Services::authorization();
+
+        $group_model = new GroupModel();
+
+        $group = $group_model
+            ->select('auth_groups.*')
+            ->join(
+                'auth_groups_users',
+                'auth_groups_users.group_id = auth_groups.id'
+            )
+            ->where('user_id', $this->user->id)
+            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
+            ->first();
+
+        $authorize->removeUserFromGroup(
+            (int) $this->user->id,
+            (int) $group->id
+        );
+
+        $authorize->addUserToGroup(
+            (int) $this->user->id,
+            (int) $this->request->getPost('role')
+        );
+
+        return redirect()->route('contributor_list', [$this->podcast->id]);
+    }
+
+    public function remove()
+    {
+        $authorize = Services::authorization();
+
+        $group_model = new GroupModel();
+
+        $group = $group_model
+            ->select('auth_groups.*')
+            ->join(
+                'auth_groups_users',
+                'auth_groups_users.group_id = auth_groups.id'
+            )
+            ->like('name', 'podcasts:' . $this->podcast->id, 'after')
+            ->where('user_id', $this->user->id)
+            ->first();
+
+        $authorize->removeUserFromGroup(
+            (int) $this->user->id,
+            (int) $group->id
+        );
+
+        (new PodcastModel())->removeContributorFromPodcast(
+            $this->user->id,
+            $this->podcast->id
+        );
+
+        return redirect()->route('contributor_list', [$this->podcast->id]);
+    }
+}
diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php
index 105aed974e202994e1a5bb5aacac7508fbf592da..2dc9aabc2edf425d1f452fe7441d99c713552497 100644
--- a/app/Controllers/Admin/Episode.php
+++ b/app/Controllers/Admin/Episode.php
@@ -17,23 +17,52 @@ class Episode extends BaseController
 
     public function _remap($method, ...$params)
     {
+        switch ($method) {
+            case 'list':
+                if (
+                    !has_permission('episodes-list') ||
+                    !has_permission("podcasts:$params[0]:episodes-list")
+                ) {
+                    throw new \RuntimeException(
+                        lang('Auth.notEnoughPrivilege')
+                    );
+                }
+            case 'edit':
+                if (
+                    !has_permission('episodes-edit') ||
+                    !has_permission("podcasts:$params[0]:episodes-edit")
+                ) {
+                    throw new \RuntimeException(
+                        lang('Auth.notEnoughPrivilege')
+                    );
+                }
+            case 'delete':
+                if (
+                    !has_permission('episodes-delete') ||
+                    !has_permission("podcasts:$params[0]:episodes-delete")
+                ) {
+                    throw new \RuntimeException(
+                        lang('Auth.notEnoughPrivilege')
+                    );
+                }
+        }
+
         $podcast_model = new PodcastModel();
 
-        $this->podcast = $podcast_model->where('name', $params[0])->first();
+        $this->podcast = $podcast_model->find($params[0]);
 
         if (count($params) > 1) {
             $episode_model = new EpisodeModel();
             if (
-                !($episode = $episode_model
+                !($this->episode = $episode_model
                     ->where([
-                        'podcast_id' => $this->podcast->id,
-                        'slug' => $params[1],
+                        'id' => $params[1],
+                        'podcast_id' => $params[0],
                     ])
                     ->first())
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
-            $this->episode = $episode;
         }
 
         return $this->$method();
@@ -41,13 +70,8 @@ class Episode extends BaseController
 
     public function list()
     {
-        $episode_model = new EpisodeModel();
-
         $data = [
             'podcast' => $this->podcast,
-            'all_podcast_episodes' => $episode_model
-                ->where('podcast_id', $this->podcast->id)
-                ->find(),
         ];
 
         return view('admin/episode/list', $data);
@@ -57,105 +81,118 @@ class Episode extends BaseController
     {
         helper(['form']);
 
-        if (
-            !$this->validate([
-                'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
-                'image' =>
-                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
-                'title' => 'required',
-                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
-                'description' => 'required',
-                'type' => 'required',
-            ])
-        ) {
-            $data = [
-                'podcast' => $this->podcast,
-            ];
-
-            echo view('admin/episode/create', $data);
-        } else {
-            $new_episode = new \App\Entities\Episode([
-                'podcast_id' => $this->podcast->id,
-                'title' => $this->request->getVar('title'),
-                'slug' => $this->request->getVar('slug'),
-                'enclosure' => $this->request->getFile('enclosure'),
-                'pub_date' => $this->request->getVar('pub_date'),
-                'description' => $this->request->getVar('description'),
-                'image' => $this->request->getFile('image'),
-                'explicit' => $this->request->getVar('explicit') or false,
-                'number' => $this->request->getVar('episode_number'),
-                'season_number' => $this->request->getVar('season_number'),
-                'type' => $this->request->getVar('type'),
-                'author_name' => $this->request->getVar('author_name'),
-                'author_email' => $this->request->getVar('author_email'),
-                'block' => $this->request->getVar('block') or false,
-            ]);
+        $data = [
+            'podcast' => $this->podcast,
+        ];
 
-            $episode_model = new EpisodeModel();
-            $episode_model->save($new_episode);
+        echo view('admin/episode/create', $data);
+    }
 
-            return redirect()->route('episode_list', [$this->podcast->name]);
+    public function attemptCreate()
+    {
+        $rules = [
+            'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
+            'image' =>
+                'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
         }
+
+        $new_episode = new \App\Entities\Episode([
+            'podcast_id' => $this->podcast->id,
+            'title' => $this->request->getPost('title'),
+            'slug' => $this->request->getPost('slug'),
+            'enclosure' => $this->request->getFile('enclosure'),
+            'pub_date' => $this->request->getPost('pub_date'),
+            'description' => $this->request->getPost('description'),
+            'image' => $this->request->getFile('image'),
+            'explicit' => (bool) $this->request->getPost('explicit'),
+            'number' => $this->request->getPost('episode_number'),
+            'season_number' => $this->request->getPost('season_number'),
+            'type' => $this->request->getPost('type'),
+            'author_name' => $this->request->getPost('author_name'),
+            'author_email' => $this->request->getPost('author_email'),
+            'block' => (bool) $this->request->getPost('block'),
+        ]);
+
+        $episode_model = new EpisodeModel();
+
+        if (!$episode_model->save($new_episode)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $episode_model->errors());
+        }
+
+        return redirect()->route('episode_list', [$this->podcast->id]);
     }
 
     public function edit()
     {
         helper(['form']);
 
-        if (
-            !$this->validate([
-                'enclosure' =>
-                    'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
-                'image' =>
-                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
-                'title' => 'required',
-                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
-                'description' => 'required',
-                'type' => 'required',
-            ])
-        ) {
-            $data = [
-                'podcast' => $this->podcast,
-                'episode' => $this->episode,
-            ];
-
-            echo view('admin/episode/edit', $data);
-        } else {
-            $this->episode->title = $this->request->getVar('title');
-            $this->episode->slug = $this->request->getVar('slug');
-            $this->episode->pub_date = $this->request->getVar('pub_date');
-            $this->episode->description = $this->request->getVar('description');
-            $this->episode->explicit =
-                ($this->request->getVar('explicit') or false);
-            $this->episode->number = $this->request->getVar('episode_number');
-            $this->episode->season_number = $this->request->getVar(
-                'season_number'
-            )
-                ? $this->request->getVar('season_number')
-                : null;
-            $this->episode->type = $this->request->getVar('type');
-            $this->episode->author_name = $this->request->getVar('author_name');
-            $this->episode->author_email = $this->request->getVar(
-                'author_email'
-            );
-            $this->episode->block = ($this->request->getVar('block') or false);
-
-            $enclosure = $this->request->getFile('enclosure');
-            if ($enclosure->isValid()) {
-                $this->episode->enclosure = $this->request->getFile(
-                    'enclosure'
-                );
-            }
-            $image = $this->request->getFile('image');
-            if ($image) {
-                $this->episode->image = $this->request->getFile('image');
-            }
+        $data = [
+            'podcast' => $this->podcast,
+            'episode' => $this->episode,
+        ];
 
-            $episode_model = new EpisodeModel();
-            $episode_model->save($this->episode);
+        echo view('admin/episode/edit', $data);
+    }
+
+    public function attemptEdit()
+    {
+        $rules = [
+            'enclosure' =>
+                'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
+            'image' =>
+                'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
 
-            return redirect()->route('episode_list', [$this->podcast->name]);
+        $this->episode->title = $this->request->getPost('title');
+        $this->episode->slug = $this->request->getPost('slug');
+        $this->episode->pub_date = $this->request->getPost('pub_date');
+        $this->episode->description = $this->request->getPost('description');
+        $this->episode->explicit = (bool) $this->request->getPost('explicit');
+        $this->episode->number = $this->request->getPost('episode_number');
+        $this->episode->season_number = $this->request->getPost('season_number')
+            ? $this->request->getPost('season_number')
+            : null;
+        $this->episode->type = $this->request->getPost('type');
+        $this->episode->author_name = $this->request->getPost('author_name');
+        $this->episode->author_email = $this->request->getPost('author_email');
+        $this->episode->block = (bool) $this->request->getPost('block');
+
+        $enclosure = $this->request->getFile('enclosure');
+        if ($enclosure->isValid()) {
+            $this->episode->enclosure = $enclosure;
         }
+        $image = $this->request->getFile('image');
+        if ($image) {
+            $this->episode->image = $image;
+        }
+
+        $episode_model = new EpisodeModel();
+
+        if (!$episode_model->save($this->episode)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $episode_model->errors());
+        }
+
+        return redirect()->route('episode_list', [$this->podcast->id]);
     }
 
     public function delete()
@@ -163,6 +200,6 @@ class Episode extends BaseController
         $episode_model = new EpisodeModel();
         $episode_model->delete($this->episode->id);
 
-        return redirect()->route('episode_list', [$this->podcast->name]);
+        return redirect()->route('episode_list', [$this->podcast->id]);
     }
 }
diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php
index 502f15322ade638c1486d3a91f1a69e5f24b265c..3fa3110b4e7f7107f36cfd29947d8033fbeab737 100644
--- a/app/Controllers/Admin/Podcast.php
+++ b/app/Controllers/Admin/Podcast.php
@@ -6,7 +6,6 @@
  */
 namespace App\Controllers\Admin;
 
-use App\Entities\UserPodcast;
 use App\Models\CategoryModel;
 use App\Models\LanguageModel;
 use App\Models\PodcastModel;
@@ -18,18 +17,59 @@ class Podcast extends BaseController
     public function _remap($method, ...$params)
     {
         if (count($params) > 0) {
+            switch ($method) {
+                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')
+                        );
+                    }
+                case 'listContributors':
+                case 'addContributor':
+                case 'editContributor':
+                case 'deleteContributor':
+                    if (
+                        !has_permission('podcasts-manage_contributors') ||
+                        !has_permission(
+                            "podcasts:$params[0]-manage_contributors"
+                        )
+                    ) {
+                        throw new \RuntimeException(
+                            lang('Auth.notEnoughPrivilege')
+                        );
+                    }
+            }
+
             $podcast_model = new PodcastModel();
-            if (
-                !($podcast = $podcast_model->where('name', $params[0])->first())
-            ) {
+            if (!($this->podcast = $podcast_model->find($params[0]))) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
-            $this->podcast = $podcast;
         }
 
         return $this->$method();
     }
 
+    public function myPodcasts()
+    {
+        $data = [
+            'all_podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
+        ];
+
+        return view('admin/podcast/list', $data);
+    }
+
     public function list()
     {
         $podcast_model = new PodcastModel();
@@ -42,133 +82,141 @@ class Podcast extends BaseController
     public function create()
     {
         helper(['form', 'misc']);
+
+        $languageModel = new LanguageModel();
+        $categoryModel = new CategoryModel();
+        $data = [
+            'languages' => $languageModel->findAll(),
+            'categories' => $categoryModel->findAll(),
+            'browser_lang' => get_browser_language(
+                $this->request->getServer('HTTP_ACCEPT_LANGUAGE')
+            ),
+        ];
+
+        echo view('admin/podcast/create', $data);
+    }
+
+    public function attemptCreate()
+    {
+        $rules = [
+            'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        $podcast = new \App\Entities\Podcast([
+            'title' => $this->request->getPost('title'),
+            'name' => $this->request->getPost('name'),
+            'description' => $this->request->getPost('description'),
+            'episode_description_footer' => $this->request->getPost(
+                'episode_description_footer'
+            ),
+            'image' => $this->request->getFile('image'),
+            'language' => $this->request->getPost('language'),
+            'category' => $this->request->getPost('category'),
+            'explicit' => (bool) $this->request->getPost('explicit'),
+            'author_name' => $this->request->getPost('author_name'),
+            'author_email' => $this->request->getPost('author_email'),
+            'owner' => user(),
+            'owner_name' => $this->request->getPost('owner_name'),
+            'owner_email' => $this->request->getPost('owner_email'),
+            'type' => $this->request->getPost('type'),
+            'copyright' => $this->request->getPost('copyright'),
+            'block' => (bool) $this->request->getPost('block'),
+            'complete' => (bool) $this->request->getPost('complete'),
+            'custom_html_head' => $this->request->getPost('custom_html_head'),
+        ]);
+
         $podcast_model = new PodcastModel();
+        $db = \Config\Database::connect();
 
-        if (
-            !$this->validate([
-                'title' => 'required',
-                'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
-                'description' => 'required|max_length[4000]',
-                'image' =>
-                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
-                'owner_email' => 'required|valid_email',
-                'type' => 'required',
-            ])
-        ) {
-            $languageModel = new LanguageModel();
-            $categoryModel = new CategoryModel();
-            $data = [
-                'languages' => $languageModel->findAll(),
-                'categories' => $categoryModel->findAll(),
-                'browser_lang' => get_browser_language(
-                    $this->request->getServer('HTTP_ACCEPT_LANGUAGE')
-                ),
-            ];
-
-            echo view('admin/podcast/create', $data);
-        } else {
-            $podcast = new \App\Entities\Podcast([
-                'title' => $this->request->getVar('title'),
-                'name' => $this->request->getVar('name'),
-                'description' => $this->request->getVar('description'),
-                'episode_description_footer' => $this->request->getVar(
-                    'episode_description_footer'
-                ),
-                'image' => $this->request->getFile('image'),
-                'language' => $this->request->getVar('language'),
-                'category' => $this->request->getVar('category'),
-                'explicit' => $this->request->getVar('explicit') or false,
-                'author_name' => $this->request->getVar('author_name'),
-                'author_email' => $this->request->getVar('author_email'),
-                'owner_name' => $this->request->getVar('owner_name'),
-                'owner_email' => $this->request->getVar('owner_email'),
-                'type' => $this->request->getVar('type'),
-                'copyright' => $this->request->getVar('copyright'),
-                'block' => $this->request->getVar('block') or false,
-                'complete' => $this->request->getVar('complete') or false,
-                'custom_html_head' => $this->request->getVar(
-                    'custom_html_head'
-                ),
-            ]);
-
-            $db = \Config\Database::connect();
-
-            $db->transStart();
-
-            $new_podcast_id = $podcast_model->insert($podcast, true);
-
-            $user_podcast_model = new \App\Models\UserPodcastModel();
-            $user_podcast_model->save([
-                'user_id' => user()->id,
-                'podcast_id' => $new_podcast_id,
-            ]);
+        $db->transStart();
 
+        if (!($new_podcast_id = $podcast_model->insert($podcast, true))) {
             $db->transComplete();
-
-            return redirect()->route('podcast_list', [$podcast->name]);
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $podcast_model->errors());
         }
+
+        $podcast_model->addContributorToPodcast(user()->id, $new_podcast_id);
+
+        $db->transComplete();
+
+        return redirect()->route('podcast_list');
     }
 
     public function edit()
     {
-        helper(['form', 'misc']);
+        helper('form');
 
-        if (
-            !$this->validate([
-                'title' => 'required',
-                'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
-                'description' => 'required|max_length[4000]',
-                'image' =>
-                    'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
-                'owner_email' => 'required|valid_email',
-                'type' => 'required',
-            ])
-        ) {
-            $languageModel = new LanguageModel();
-            $categoryModel = new CategoryModel();
-            $data = [
-                'podcast' => $this->podcast,
-                'languages' => $languageModel->findAll(),
-                'categories' => $categoryModel->findAll(),
-            ];
-
-            echo view('admin/podcast/edit', $data);
-        } else {
-            $this->podcast->title = $this->request->getVar('title');
-            $this->podcast->name = $this->request->getVar('name');
-            $this->podcast->description = $this->request->getVar('description');
-            $this->podcast->episode_description_footer = $this->request->getVar(
-                'episode_description_footer'
-            );
+        $languageModel = new LanguageModel();
+        $categoryModel = new CategoryModel();
+        $data = [
+            'podcast' => $this->podcast,
+            'languages' => $languageModel->findAll(),
+            'categories' => $categoryModel->findAll(),
+        ];
 
-            $image = $this->request->getFile('image');
-            if ($image->isValid()) {
-                $this->podcast->image = $this->request->getFile('image');
-            }
-            $this->podcast->language = $this->request->getVar('language');
-            $this->podcast->category = $this->request->getVar('category');
-            $this->podcast->explicit =
-                ($this->request->getVar('explicit') or false);
-            $this->podcast->author_name = $this->request->getVar('author_name');
-            $this->podcast->author_email = $this->request->getVar(
-                'author_email'
-            );
-            $this->podcast->owner_name = $this->request->getVar('owner_name');
-            $this->podcast->owner_email = $this->request->getVar('owner_email');
-            $this->podcast->type = $this->request->getVar('type');
-            $this->podcast->copyright = $this->request->getVar('copyright');
-            $this->podcast->block = ($this->request->getVar('block') or false);
-            $this->podcast->complete =
-                ($this->request->getVar('complete') or false);
-            $this->podcast->custom_html_head = $this->request->getVar(
-                'custom_html_head'
-            );
+        echo view('admin/podcast/edit', $data);
+    }
 
-            $podcast_model = new PodcastModel();
-            $podcast_model->save($this->podcast);
+    public function attemptEdit()
+    {
+        $rules = [
+            'image' =>
+                'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
+        ];
+
+        if (!$this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        $this->podcast->title = $this->request->getPost('title');
+        $this->podcast->name = $this->request->getPost('name');
+        $this->podcast->description = $this->request->getPost('description');
+        $this->podcast->episode_description_footer = $this->request->getPost(
+            'episode_description_footer'
+        );
+
+        $image = $this->request->getFile('image');
+        if ($image->isValid()) {
+            $this->podcast->image = $image;
+        }
+        $this->podcast->language = $this->request->getPost('language');
+        $this->podcast->category = $this->request->getPost('category');
+        $this->podcast->explicit = (bool) $this->request->getPost('explicit');
+        $this->podcast->author_name = $this->request->getPost('author_name');
+        $this->podcast->author_email = $this->request->getPost('author_email');
+        $this->podcast->owner_name = $this->request->getPost('owner_name');
+        $this->podcast->owner_email = $this->request->getPost('owner_email');
+        $this->podcast->type = $this->request->getPost('type');
+        $this->podcast->copyright = $this->request->getPost('copyright');
+        $this->podcast->block = (bool) $this->request->getPost('block');
+        $this->podcast->complete = (bool) $this->request->getPost('complete');
+        $this->podcast->custom_html_head = $this->request->getPost(
+            'custom_html_head'
+        );
 
-            return redirect()->route('podcast_list', [$this->podcast->name]);
+        $podcast_model = new PodcastModel();
+
+        if (!$podcast_model->save($this->podcast)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $podcast_model->errors());
         }
+
+        return redirect()->route('podcast_list');
     }
 
     public function delete()
diff --git a/app/Controllers/Admin/User.php b/app/Controllers/Admin/User.php
index 4faffc236339bf10dff8cab77c9eb1deadb6b671..4a8342f50a37ee220d3c9f612b3fc50005a35d7e 100644
--- a/app/Controllers/Admin/User.php
+++ b/app/Controllers/Admin/User.php
@@ -17,12 +17,9 @@ class User extends BaseController
     {
         if (count($params) > 0) {
             $user_model = new UserModel();
-            if (
-                !($user = $user_model->where('username', $params[0])->first())
-            ) {
+            if (!($this->user = $user_model->find($params[0]))) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
-            $this->user = $user;
         }
 
         return $this->$method();
@@ -38,6 +35,11 @@ class User extends BaseController
     }
 
     public function create()
+    {
+        echo view('admin/user/create');
+    }
+
+    public function attemptCreate()
     {
         $user_model = new UserModel();
 
@@ -53,30 +55,33 @@ class User extends BaseController
         );
 
         if (!$this->validate($rules)) {
-            echo view('admin/user/create');
-        } else {
-            // Save the user
-            $user = new \Myth\Auth\Entities\User($this->request->getPost());
-
-            // Activate user
-            $user->activate();
-
-            // Force user to reset his password on first connection
-            $user->force_pass_reset = true;
-            $user->generateResetHash();
-
-            if (!$user_model->save($user)) {
-                return redirect()
-                    ->back()
-                    ->withInput()
-                    ->with('errors', $user_model->errors());
-            }
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        // Save the user
+        $user = new \Myth\Auth\Entities\User($this->request->getPost());
 
-            // Success!
+        // Activate user
+        $user->activate();
+
+        // Force user to reset his password on first connection
+        $user->force_pass_reset = true;
+        $user->generateResetHash();
+
+        if (!$user_model->save($user)) {
             return redirect()
-                ->route('user_list')
-                ->with('message', lang('User.createSuccess'));
+                ->back()
+                ->withInput()
+                ->with('errors', $user_model->errors());
         }
+
+        // Success!
+        return redirect()
+            ->route('user_list')
+            ->with('message', lang('User.createSuccess'));
     }
 
     public function forcePassReset()
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 4721a6582c5ebd2c1ef39e29184893bb74d7ad9e..b5174ad637cad6906d8e6b348072cd583b0ebe63 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -24,7 +24,7 @@ class Episode extends BaseController
         if (count($params) > 1) {
             $episode_model = new EpisodeModel();
             if (
-                !($episode = $episode_model
+                !($this->episode = $episode_model
                     ->where([
                         'podcast_id' => $this->podcast->id,
                         'slug' => $params[1],
@@ -33,7 +33,6 @@ class Episode extends BaseController
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
-            $this->episode = $episode;
         }
 
         return $this->$method();
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 70db9d60594f0dc1e59690d08a9933e2206ada90..5fd0e14123361ee3459a1b6e88e154a77cd7bafb 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -17,11 +17,12 @@ class Podcast extends BaseController
         if (count($params) > 0) {
             $podcast_model = new PodcastModel();
             if (
-                !($podcast = $podcast_model->where('name', $params[0])->first())
+                !($this->podcast = $podcast_model
+                    ->where('name', $params[0])
+                    ->first())
             ) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
             }
-            $this->podcast = $podcast;
         }
 
         return $this->$method();
diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index 4cf12a612bd9d241e4d77f45c21e2d0a4b05446f..cd35fe3797a4040496684ee005bf7d8555652b99 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -81,17 +81,23 @@ class AddPodcasts extends Migration
                     'Email of the group responsible for creating the show.  Show author most often refers to the parent company or network of a podcast, but it can also be used to identify the host(s) if none exists.  Author information is especially useful if a company or organization publishes multiple podcasts. Providing this information will allow listeners to see all shows created by the same entity.',
                 'null' => true,
             ],
+            'owner_id' => [
+                'type' => 'INT',
+                'constraint' => 11,
+                'unsigned' => true,
+                'comment' => 'The podcast owner.',
+            ],
             'owner_name' => [
                 'type' => 'VARCHAR',
                 'constraint' => 1024,
-                'owner_name' =>
+                'comment' =>
                     'The podcast owner name.  Note: The owner information is for administrative communication about the podcast and isn’t displayed in Apple Podcasts.',
                 'null' => true,
             ],
             'owner_email' => [
                 'type' => 'VARCHAR',
                 'constraint' => 1024,
-                'owner_email' =>
+                'comment' =>
                     'The podcast owner email address.  Note: The owner information is for administrative communication about the podcast and isn’t displayed in Apple Podcasts. Please make sure the email address is active and monitored.',
                 'null' => true,
             ],
@@ -147,6 +153,7 @@ class AddPodcasts extends Migration
             ],
         ]);
         $this->forge->addKey('id', true);
+        $this->forge->addForeignKey('owner_id', 'users', 'id');
         $this->forge->createTable('podcasts');
     }
 
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 16381819dce947c06352d355de6eb0073c09ea97..abed46d5166f9a33ddd356aa48297e487bb3716d 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
@@ -17,12 +17,6 @@ class AddUsersPodcasts extends Migration
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'user_id' => [
                 'type' => 'INT',
                 'constraint' => 11,
@@ -34,8 +28,7 @@ class AddUsersPodcasts extends Migration
                 'unsigned' => true,
             ],
         ]);
-        $this->forge->addPrimaryKey('id');
-        $this->forge->addUniqueKey(['user_id', 'podcast_id']);
+        $this->forge->addPrimaryKey(['user_id', 'podcast_id']);
         $this->forge->addForeignKey('user_id', 'users', 'id');
         $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
         $this->forge->createTable('users_podcasts');
diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php
new file mode 100644
index 0000000000000000000000000000000000000000..cbbbe4f1e54e7a9ecba7140b4e576c602d3e9b13
--- /dev/null
+++ b/app/Database/Seeds/AuthSeeder.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Class PermissionSeeder
+ * Inserts permissions
+ *
+ * @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 AuthSeeder extends Seeder
+{
+    public function run()
+    {
+        helper('auth');
+
+        $groups = [['id' => 1, 'name' => 'superadmin', 'description' => '']];
+
+        /** 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' => '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',
+                ],
+            ],
+        ];
+
+        // Map permissions to a format the `auth_permissions` table expects
+        $data_permissions = [];
+        $data_groups_permissions = [];
+        $permission_id = 0;
+        foreach ($permissions as $context => $actions) {
+            foreach ($actions as $action) {
+                array_push($data_permissions, [
+                    'id' => ++$permission_id,
+                    'name' => get_permission($context, $action['name']),
+                    'description' => $action['description'],
+                ]);
+
+                // add all permissions to superadmin
+                array_push($data_groups_permissions, [
+                    'group_id' => 1,
+                    'permission_id' => $permission_id,
+                ]);
+            }
+        }
+
+        $this->db->table('auth_permissions')->insertBatch($data_permissions);
+        $this->db->table('auth_groups')->insertBatch($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/UserSeeder.php b/app/Database/Seeds/UserSeeder.php
deleted file mode 100644
index 826f1332a17737dfbb62ff7f5ec6db90a46cf6f6..0000000000000000000000000000000000000000
--- a/app/Database/Seeds/UserSeeder.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-/**
- * Class UserSeeder
- * Inserts 'admin' user in users table for testing purposes
- *
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Database\Seeds;
-
-use CodeIgniter\Database\Seeder;
-
-class UserSeeder extends Seeder
-{
-    public function run()
-    {
-        $data = [
-            'username' => 'admin',
-            'email' => 'admin@castopod.com',
-            'password_hash' =>
-                // password: AGUehL3P
-                '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
-            'active' => 1,
-        ];
-
-        $this->db->table('users')->insert($data);
-    }
-}
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 386be65c3f3d84032f74f0cdeaeb16fe227bd05f..edd59e2b10cf89a34b3f2ab91bbebe6c28529450 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -9,6 +9,7 @@ namespace App\Entities;
 
 use App\Models\EpisodeModel;
 use CodeIgniter\Entity;
+use Myth\Auth\Models\UserModel;
 
 class Podcast extends Entity
 {
@@ -17,6 +18,8 @@ class Podcast extends Entity
     protected $image_media_path;
     protected $image_url;
     protected $episodes;
+    protected $owner;
+    protected $contributors;
 
     protected $casts = [
         'id' => 'integer',
@@ -29,6 +32,7 @@ class Podcast extends Entity
         'explicit' => 'boolean',
         'author_name' => '?string',
         'author_email' => '?string',
+        'owner_id' => 'integer',
         'owner_name' => '?string',
         'owner_email' => '?string',
         'type' => 'string',
@@ -92,7 +96,7 @@ class Podcast extends Entity
             );
         }
 
-        if (empty($this->permissions)) {
+        if (empty($this->episodes)) {
             $this->episodes = (new EpisodeModel())->getPodcastEpisodes(
                 $this->id
             );
@@ -100,4 +104,40 @@ class Podcast extends Entity
 
         return $this->episodes;
     }
+
+    /**
+     * Returns the podcast owner
+     *
+     * @return \Myth\Auth\Entities\User
+     */
+    public function getOwner()
+    {
+        if (empty($this->id)) {
+            throw new \RuntimeException(
+                'Podcast must be created before getting owner.'
+            );
+        }
+
+        if (empty($this->owner)) {
+            $this->owner = (new UserModel())->find($this->owner_id);
+        }
+
+        return $this->owner;
+    }
+
+    public function setOwner(\Myth\Auth\Entities\User $user)
+    {
+        $this->attributes['owner_id'] = $user->id;
+
+        return $this;
+    }
+
+    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();
+    }
 }
diff --git a/app/Entities/UserPodcast.php b/app/Entities/UserPodcast.php
deleted file mode 100644
index d951557dc9e432a3d682dc72e99e61d90c9a51f9..0000000000000000000000000000000000000000
--- a/app/Entities/UserPodcast.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Entities;
-
-use CodeIgniter\Entity;
-
-class UserPodcast extends Entity
-{
-    protected $casts = [
-        'user_id' => 'integer',
-        'podcast_id' => 'integer',
-    ];
-}
diff --git a/app/Helpers/auth_helper.php b/app/Helpers/auth_helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..344e0636a2a8bd632996c8a080ee4670e5c5c1c1
--- /dev/null
+++ b/app/Helpers/auth_helper.php
@@ -0,0 +1,19 @@
+<?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/Language/en/Contributor.php b/app/Language/en/Contributor.php
new file mode 100644
index 0000000000000000000000000000000000000000..552c9f64fc946619b13bde5979b1c6f3df8f9027
--- /dev/null
+++ b/app/Language/en/Contributor.php
@@ -0,0 +1,21 @@
+<?
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+return [
+    'podcast_contributors' => 'Podcast contributors',
+    'add' => 'Add contributor',
+    'add_contributor' => 'Add a contributor for {0}',
+    'edit_role' => 'Update role for {0}',
+    'edit' => 'Edit',
+    'remove' => 'Remove',
+    'form' => [
+        'user' => 'User',
+        'role' => 'Role',
+        'submit_add' => 'Add contributor',
+        'submit_edit' => 'Update role'
+    ]
+];
diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php
index 22ff6a53495130f73b3bc476e0ffe7da506df3e3..80ae2df6b3971850038d8c27fc9dbd80cd84e68e 100644
--- a/app/Language/en/Episode.php
+++ b/app/Language/en/Episode.php
@@ -7,7 +7,6 @@
 
 return [
     'all_podcast_episodes' => 'All podcast episodes',
-    'create_one' => 'Add a new one',
     'back_to_podcast' => 'Go back to podcast',
     'edit' => 'Edit',
     'delete' => 'Delete',
diff --git a/app/Language/en/MyAccount.php b/app/Language/en/MyAccount.php
index c1935bcc3331216129afd04242244f2162a914fd..f32684b7eeaa6a2e5237149d1835e004a4789f61 100644
--- a/app/Language/en/MyAccount.php
+++ b/app/Language/en/MyAccount.php
@@ -7,5 +7,6 @@
 
 return [
     'passwordChangeSuccess' => 'Password has been successfully changed!',
-    'changePassword' => 'Change my password'
+    'changePassword' => 'Change my password',
+    'info' => 'My account info'
 ];
diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php
index 767d54e08970bf74283c1a8b2542c19a86118f9a..54601135025fcaa163387110efeacefb427fbe9a 100644
--- a/app/Language/en/Podcast.php
+++ b/app/Language/en/Podcast.php
@@ -8,13 +8,13 @@
 return [
     'all_podcasts' => 'All podcasts',
     'no_podcast' => 'No podcast found!',
-    'create_one' => 'Add a new one',
     'create' => 'Create a Podcast',
     'new_episode' => 'New Episode',
     'feed' => 'RSS feed',
     'edit' => 'Edit',
     'delete' => 'Delete',
     'see_episodes' => 'See episodes',
+    'see_contributors' => 'See contributors',
     'goto_page' => 'Go to page',
     'form' => [
         'title' => 'Title',
diff --git a/app/Language/en/User.php b/app/Language/en/User.php
index fcf9134c643a0023c2d5024e153a30ef95f3a4af..c9e0fc03ccd8d15f37375a507c4b53acb1c4f7f4 100644
--- a/app/Language/en/User.php
+++ b/app/Language/en/User.php
@@ -10,11 +10,13 @@ return [
     '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.',
     'forcePassReset' => 'Force pass reset',
     'ban' => 'Ban',
     'unban' => 'Unban',
     'delete' => 'Delete',
     'create' => 'Create a user',
+    'all_users' => 'All users',
     'form' => [
         'email' => 'Email',
         'username' => 'Username',
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index ed2ea4e9f84b5e70c4e8d040c0f4f9e5cd2adcee..eb372fa4018efc6961f77c87aedf562953311009 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -19,11 +19,8 @@ class EpisodeModel extends Model
         'title',
         'slug',
         'enclosure_uri',
-        'enclosure_length',
-        'enclosure_type',
         'pub_date',
         'description',
-        'duration',
         'image_uri',
         'explicit',
         'number',
@@ -39,6 +36,21 @@ class EpisodeModel extends Model
     protected $useSoftDeletes = true;
     protected $useTimestamps = true;
 
+    protected $validationRules = [
+        'podcast_id' => 'required',
+        'title' => 'required',
+        'slug' => 'required|regex_match[/^[a-zA-Z0-9\-]{1,191}$/]',
+        'enclosure_uri' => 'required',
+        'pub_date' => 'required|valid_date',
+        'description' => 'required',
+        'image_uri' => 'required',
+        'number' => 'required',
+        'season_number' => 'required',
+        'author_email' => 'valid_email|permit_empty',
+        'type' => 'required',
+    ];
+    protected $validationMessages = [];
+
     protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
     protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache'];
 
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 09712b6a4e63c035c0471feaf84c2d2db9740e31..8d7365a98362030d6b7dbe8cc55c55a23d4114d1 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -8,6 +8,8 @@
 namespace App\Models;
 
 use CodeIgniter\Model;
+use Myth\Auth\Authorization\GroupModel;
+use Myth\Auth\Config\Services;
 
 class PodcastModel extends Model
 {
@@ -26,6 +28,7 @@ class PodcastModel extends Model
         'explicit',
         'author_name',
         'author_email',
+        'owner_id',
         'owner_name',
         'owner_email',
         'type',
@@ -40,8 +43,60 @@ class PodcastModel extends Model
 
     protected $useTimestamps = true;
 
-    protected $afterInsert = ['clearCache'];
+    protected $validationRules = [
+        'title' => 'required',
+        'name' =>
+            'required|regex_match[/^[a-zA-Z0-9\_]{1,191}$/]|is_unique[podcasts.name,id,{id}]',
+        'description' => 'required',
+        'image_uri' => 'required',
+        'language' => 'required',
+        'category' => 'required',
+        'author_email' => 'valid_email|permit_empty',
+        'owner_id' => 'required',
+        'owner_email' => 'required|valid_email',
+        'type' => 'required',
+    ];
+    protected $validationMessages = [];
+
+    protected $afterInsert = ['clearCache', 'createPodcastPermissions'];
     protected $afterUpdate = ['clearCache'];
+    protected $beforeDelete = ['clearCache'];
+
+    /**
+     *  Gets all the podcasts a given user is contributing to
+     *
+     * @param int $user_id
+     *
+     * @return \App\Entities\Podcast[] podcasts
+     */
+    public function getUserPodcasts($user_id)
+    {
+        return $this->select('podcasts.*')
+            ->join('users_podcasts', 'users_podcasts.podcast_id = podcasts.id')
+            ->where('users_podcasts.user_id', $user_id)
+            ->findAll();
+    }
+
+    public function addContributorToPodcast($user_id, $podcast_id)
+    {
+        $data = [
+            'user_id' => (int) $user_id,
+            'podcast_id' => (int) $podcast_id,
+        ];
+
+        return $this->db->table('users_podcasts')->insert($data);
+    }
+
+    public function removeContributorFromPodcast($user_id, $podcast_id)
+    {
+        return $this->db
+            ->table('users_podcasts')
+            ->where([
+                'user_id' => $user_id,
+                'podcast_id' => $podcast_id,
+            ])
+            ->delete();
+    }
 
     protected function clearCache(array $data)
     {
@@ -56,5 +111,95 @@ class PodcastModel extends Model
         // foreach ($podcast->episodes as $episode) {
         //     $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' => '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/UserPodcastModel.php b/app/Models/UserPodcastModel.php
deleted file mode 100644
index 32d36b051269f913ae38f1c928fe64031ddccbd4..0000000000000000000000000000000000000000
--- a/app/Models/UserPodcastModel.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-/**
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class UserPodcastModel extends Model
-{
-    protected $table = 'users_podcasts';
-    protected $primaryKey = 'id';
-
-    protected $allowedFields = ['user_id', 'podcast_id'];
-
-    protected $returnType = 'App\Entities\UserPodcast';
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Views/_message_block.php b/app/Views/_message_block.php
index 466b5780b94e4651ae0bd30f95c79bf3b13740e8..2504c21135b155c11917ede4f2a5e7abbadf17aa 100644
--- a/app/Views/_message_block.php
+++ b/app/Views/_message_block.php
@@ -1,17 +1,17 @@
 <?php if (session()->has('message')): ?>
-	<div class="px-4 py-2 font-semibold text-green-900 bg-green-200 border border-green-700">
+	<div class="px-4 py-2 mb-4 font-semibold text-green-900 bg-green-200 border border-green-700">
 		<?= session('message') ?>
 	</div>
 <?php endif; ?>
 
 <?php if (session()->has('error')): ?>
-	<div class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700">
+	<div class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700">
 		<?= session('error') ?>
 	</div>
 <?php endif; ?>
 
 <?php if (session()->has('errors')): ?>
-	<ul class="px-4 py-2 font-semibold text-red-900 bg-red-200 border border-red-700">
+	<ul class="px-4 py-2 mb-4 font-semibold text-red-900 bg-red-200 border border-red-700">
 	<?php foreach (session('errors') as $error): ?>
 		<li><?= $error ?></li>
 	<?php endforeach; ?>
diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php
index a96b0a7cf4daea8a3d651546dceca4559842f344..711a7ae702b1c9243a301571b32526949e8e2eb6 100644
--- a/app/Views/admin/_layout.php
+++ b/app/Views/admin/_layout.php
@@ -3,7 +3,7 @@
 
 <head>
 	<meta charset="UTF-8">
-	<title>Castopod</title>
+	<title>Castopod Admin</title>
 	<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.">
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
@@ -12,9 +12,12 @@
 
 <body class="flex flex-col min-h-screen mx-auto">
 	<header class="text-white bg-gray-900 border-b">
-		<div class="flex items-center justify-between px-4 py-4 mx-auto">
-			<a href="<?= route_to('home') ?>" class="text-xl">Castopod Admin</a>
-			<nav>
+		<div class="flex items-center px-4 py-4 mx-auto">
+			<a href="<?= route_to('admin') ?>" class="text-xl">Castopod Admin</a>
+			<a href="<?= route_to(
+       'home'
+   ) ?>" class="ml-4 text-sm underline hover:no-underline">Go to website</a>
+			<nav class="ml-auto">
 				<span class="mr-2">Welcome, <?= user()->username ?></span>
 				<a class="px-4 py-2 border hover:bg-gray-800" href="<?= route_to(
         'logout'
@@ -25,9 +28,8 @@
 	<div class="flex flex-1">
 		<?= view('admin/_sidenav') ?>
 		<main class="container flex-1 px-4 py-6 mx-auto">
-			<div class="mb-4">
-				<?= view('_message_block') ?>
-			</div>
+			<h1 class="mb-4 text-2xl"><?= $this->renderSection('title') ?></h1>
+			<?= view('_message_block') ?>
 			<?= $this->renderSection('content') ?>
 		</main>
 	</div>
diff --git a/app/Views/admin/_sidenav.php b/app/Views/admin/_sidenav.php
index f68ae5bab786e2dead2f4471e5f1da31034984ed..da7274334f2b591baf0d3b649f426605951fdf93 100644
--- a/app/Views/admin/_sidenav.php
+++ b/app/Views/admin/_sidenav.php
@@ -8,6 +8,11 @@
         <div class="mb-4">
             <span class="mb-3 text-sm font-bold tracking-wide text-gray-600 uppercase lg:mb-2 lg:text-xs">Podcasts</span>
             <ul>
+                <li>
+                    <a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
+                        'my_podcasts'
+                    ) ?>">My podcasts</a>
+                </li>
                 <li>
                     <a class="block px-2 py-1 -mx-2 text-gray-600 transition duration-200 ease-in-out hover:text-gray-900" href="<?= route_to(
                         'podcast_list'
diff --git a/app/Views/admin/contributor/add.php b/app/Views/admin/contributor/add.php
new file mode 100644
index 0000000000000000000000000000000000000000..69bbe71a3957386f43d8df5f3755d739d854ac73
--- /dev/null
+++ b/app/Views/admin/contributor/add.php
@@ -0,0 +1,48 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Contributor.add_contributor', [$podcast->title]) ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+<form action="<?= route_to(
+    'contributor_add',
+    $podcast->id
+) ?>" method="post" class="flex flex-col max-w-lg">
+    <?= csrf_field() ?>
+    
+    <div class="flex flex-col mb-4">
+        <label for="user"><?= lang('Contributor.form.user') ?></label>
+        <select id="user" name="user" autocomplete="off" class="form-select" required>
+            <?php foreach ($users as $user): ?>
+                <option value="<?= $user->id ?>"
+                <?php if (
+                    old('user') == $user->id
+                ): ?> selected <?php endif; ?>>
+                    <?= $user->username ?>
+                </option>
+            <?php endforeach; ?>
+        </select>
+    </div>
+
+    <div class="flex flex-col mb-4">
+        <label for="role"><?= lang('Contributor.form.role') ?></label>
+        <select id="role" name="role" autocomplete="off" class="form-select" required>
+            <?php foreach ($roles as $role): ?>
+                <option value="<?= $role->id ?>"
+                <?php if (
+                    old('role') == $role->id
+                ): ?> selected <?php endif; ?>>
+                    <?= $role->name ?>
+                </option>
+            <?php endforeach; ?>
+        </select>
+    </div>
+
+    <button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
+        'Contributor.form.submit_add'
+    ) ?></button>
+</form>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/contributor/edit.php b/app/Views/admin/contributor/edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..fb80c1a5fa8c94480bde03cb14174a1de2b0fb94
--- /dev/null
+++ b/app/Views/admin/contributor/edit.php
@@ -0,0 +1,35 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Contributor.edit_role', [$user->username]) ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+<form action="<?= route_to(
+    'contributor_edit',
+    $podcast->id,
+    $user->id
+) ?>" method="post" class="flex flex-col max-w-lg">
+    <?= csrf_field() ?>
+
+    <div class="flex flex-col mb-4">
+        <label for="category"><?= lang('Contributor.form.role') ?></label>
+        <select id="role" name="role" autocomplete="off" class="form-select" required>
+            <?php foreach ($roles as $role): ?>
+                <option value="<?= $role->id ?>"
+                <?php if (
+                    old('role') == $role->id
+                ): ?> selected <?php endif; ?>>
+                    <?= $role->name ?>
+                </option>
+            <?php endforeach; ?>
+        </select>
+    </div>
+
+    <button type="submit" name="submit" class="self-end px-4 py-2 bg-gray-200"><?= lang(
+        'Contributor.form.submit_edit'
+    ) ?></button>
+
+</form>
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php
new file mode 100644
index 0000000000000000000000000000000000000000..e018cb7de8a41a65de86b9784abd05148fb6b9ec
--- /dev/null
+++ b/app/Views/admin/contributor/list.php
@@ -0,0 +1,47 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Contributor.podcast_contributors') ?>
+<?= $this->endSection() ?>
+
+
+<?= $this->section('content') ?>
+<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
+    'contributor_add',
+    $podcast->id
+) ?>"><?= lang('Contributor.add') ?></a>
+
+<table class="table-auto">
+    <thead>
+        <tr>
+            <th class="px-4 py-2">Username</th>
+            <th class="px-4 py-2">Permissions</th>
+            <th class="px-4 py-2">Actions</th>
+        </tr>
+    </thead>
+    <tbody>
+        <?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">
+                <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',
+                    $podcast->id,
+                    $contributor->id
+                ) ?>"><?= lang('Contributor.edit') ?></a>
+                <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
+                    'contributor_remove',
+                    $podcast->id,
+                    $contributor->id
+                ) ?>"><?= lang('Contributor.remove') ?></a>
+            </td>
+        </tr>
+        <?php endforeach; ?>
+    </tbody>
+</table>
+
+<?= $this->endSection() ?>
diff --git a/app/Views/admin/dashboard.php b/app/Views/admin/dashboard.php
index 910003fae670ce1928fa03736eda874597882595..9bbda5012474677097a25915181de48235ddfc97 100644
--- a/app/Views/admin/dashboard.php
+++ b/app/Views/admin/dashboard.php
@@ -1,8 +1,6 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
-
-<h1 class="text-2xl">Welcome to the admin dashboard!</h1>
-
-<?= $this->endSection() ?>
+<?= $this->section(
+    'title'
+) ?>Welcome to the admin dashboard!<?= $this->endSection() ?>
 
diff --git a/app/Views/admin/episode/create.php b/app/Views/admin/episode/create.php
index af1e9d4069a4c5797c6d81d4447c3544385aaf87..b4cd9f58f3eef6b6e11eafab739b7fb14387ea79 100644
--- a/app/Views/admin/episode/create.php
+++ b/app/Views/admin/episode/create.php
@@ -1,14 +1,13 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('Episode.create') ?>
+<?= $this->endSection() ?>
 
-<h1 class="mb-6 text-xl"><?= lang('Episode.create') ?></h1>
 
-<div class="mb-8">
-    <?= \Config\Services::validation()->listErrors() ?>
-</div>
+<?= $this->section('content') ?>
 
-<?= form_open_multipart(route_to('episode_create', $podcast->name), [
+<?= form_open_multipart(route_to('episode_create', $podcast->id), [
     'method' => 'post',
     'class' => 'flex flex-col max-w-md',
 ]) ?>
@@ -21,24 +20,30 @@
 
 <div class="flex flex-col mb-4">
     <label for="title"><?= lang('Episode.form.title') ?></label>
-    <input type="text" class="form-input" id="title" name="title" required />
+    <input type="text" class="form-input" id="title" name="title" required value="<?= old(
+        'title'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="slug"><?= lang('Episode.form.slug') ?></label>
-    <input type="text" class="form-input" id="slug" name="slug" required />
+    <input type="text" class="form-input" id="slug" name="slug" required value="<?= old(
+        'slug'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="description"><?= lang('Episode.form.description') ?></label>
-    <textarea class="form-textarea" id="description" name="description" required></textarea>
+    <textarea class="form-textarea" id="description" name="description" required><?= old(
+        'description'
+    ) ?></textarea>
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="pub_date"><?= lang('Episode.form.pub_date') ?></label>
-    <input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= date(
-        'Y-m-d'
-    ) ?>" />
+    <input type="date" class="form-input" id="pub_date" name="pub_date" value="<?= old(
+        'pub_date'
+    ) || date('Y-m-d') ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
@@ -50,16 +55,22 @@
     <label for="episode_number"><?= lang(
         'Episode.form.episode_number'
     ) ?></label>
-    <input type="number" class="form-input" id="episode_number" name="episode_number" required />
+    <input type="number" class="form-input" id="episode_number" name="episode_number" required value="<?= old(
+        'episode_number'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="season_number"><?= lang('Episode.form.season_number') ?></label>
-    <input type="number" class="form-input" id="season_number" name="season_number" />
+    <input type="number" class="form-input" id="season_number" name="season_number" value="<?= old(
+        'season_number'
+    ) ?>" />
 </div>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
+    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if (
+        old('explicit')
+    ): ?> checked <?php endif; ?> />
     <label for="explicit" class="pl-2"><?= lang(
         'Episode.form.explicit'
     ) ?></label>
@@ -67,32 +78,45 @@
 
 <div class="flex flex-col mb-4">
     <label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
-    <input type="text" class="form-input" id="author_name" name="author_name" />
+    <input type="text" class="form-input" id="author_name" name="author_name" value="<?= old(
+        'author_name'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
-    <input type="email" class="form-input" id="author_email" name="author_email" />
+    <input type="email" class="form-input" id="author_email" name="author_email" value="<?= old(
+        'author_email'
+    ) ?>" />
 </div>
 
 <fieldset class="flex flex-col mb-4">
     <legend><?= lang('Episode.form.type.label') ?></legend>
     <label for="full" class="inline-flex items-center">
-        <input type="radio" class="form-radio" value="full" id="full" name="type" required checked />
+        <input type="radio" class="form-radio" value="full" id="full" name="type" required <?php if (
+            !old('type') ||
+            old('type') == 'full'
+        ): ?> checked <?php endif; ?> />
         <span class="ml-2"><?= lang('Episode.form.type.full') ?></span>  
     </label>
     <label for="trailer" class="inline-flex items-center">
-        <input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required />
+        <input type="radio" class="form-radio" value="trailer" id="trailer" name="type" required <?php if (
+            old('type') == 'trailer'
+        ): ?> checked <?php endif; ?> />
         <span class="ml-2"><?= lang('Episode.form.type.trailer') ?></span>  
     </label>
     <label for="bonus" class="inline-flex items-center">
-        <input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required />
+        <input type="radio" class="form-radio" value="bonus" id="bonus" name="type" required <?php if (
+            old('type') == 'bonus'
+        ): ?> checked <?php endif; ?> />
         <span class="ml-2"><?= lang('Episode.form.type.bonus') ?></span>  
     </label>
 </fieldset>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="block" name="block" class="form-checkbox" />
+    <input type="checkbox" id="block" name="block" class="form-checkbox" <?php if (
+        old('block')
+    ): ?> checked <?php endif; ?> />
     <label for="block" class="pl-2"><?= lang('Episode.form.block') ?></label>
 </div>
 
diff --git a/app/Views/admin/episode/edit.php b/app/Views/admin/episode/edit.php
index 51b91d984d74ccd16822daf1c8da8f1ad29ab37e..3692d768517116760c98cdd11054388ce13ca5a3 100644
--- a/app/Views/admin/episode/edit.php
+++ b/app/Views/admin/episode/edit.php
@@ -1,20 +1,16 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('Episode.edit') ?>
+<?= $this->endSection() ?>
 
-<h1 class="mb-6 text-xl"><?= lang('Episode.edit') ?></h1>
 
-<div class="mb-8">
-    <?= \Config\Services::validation()->listErrors() ?>
-</div>
+<?= $this->section('content') ?>
 
-<?= form_open_multipart(
-    route_to('episode_edit', $podcast->name, $episode->slug),
-    [
-        'method' => 'post',
-        'class' => 'flex flex-col max-w-md',
-    ]
-) ?>
+<?= form_open_multipart(route_to('episode_edit', $podcast->id, $episode->id), [
+    'method' => 'post',
+    'class' => 'flex flex-col max-w-md',
+]) ?>
 <?= csrf_field() ?>
 
 <div class="flex flex-col mb-4">
diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php
index 81d4c1f9a373ac49554c5aa0453a5e23b6a87381..54862ad4891c4d24bf3f5e9716dbca91f5c17856 100644
--- a/app/Views/admin/episode/list.php
+++ b/app/Views/admin/episode/list.php
@@ -1,21 +1,29 @@
 <?= $this->extend('admin/_layout') ?>
 
+<?= $this->section('title') ?>
+
+<?= lang('Episode.all_podcast_episodes') ?> (<?= count($podcast->episodes) ?>)
+
+<?= $this->endSection() ?>
+
+
 <?= $this->section('content') ?>
 
+<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
+    'episode_create',
+    $podcast->id
+) ?>"><?= lang('Episode.create') ?></a>
 <div class="flex flex-col py-4">
-    <h1 class="mb-4 text-xl"><?= lang(
-        'Episode.all_podcast_episodes'
-    ) ?> (<?= count($all_podcast_episodes) ?>)</h1>
-    <?php if ($all_podcast_episodes): ?>
-        <?php foreach ($all_podcast_episodes as $episode): ?>
+    <?php if ($podcast->episodes): ?>
+        <?php foreach ($podcast->episodes as $episode): ?>
             <article class="flex-col w-full max-w-lg p-4 mb-4 border shadow">
                 <div class="flex mb-2">
                     <img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 mr-4" />
                     <div class="flex flex-col flex-1">
                         <a href="<?= route_to(
                             'episode_edit',
-                            $podcast->name,
-                            $episode->slug
+                            $podcast->id,
+                            $episode->id
                         ) ?>">
                             <h3 class="text-xl font-semibold">
                                 <span class="mr-1 underline hover:no-underline"><?= $episode->title ?></span>
@@ -24,15 +32,15 @@
                             <p><?= $episode->description ?></p>
                         </a>
                         <audio controls class="mt-auto" preload="none">
-                            <source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
+                            <source src="<?= $episode->enclosure_media_path ?>" type="<?= $episode->enclosure_type ?>">
                             Your browser does not support the audio tag.
                         </audio>
                     </div>
                 </div>
                 <a class="inline-flex px-4 py-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
                     'episode_edit',
-                    $podcast->name,
-                    $episode->slug
+                    $podcast->id,
+                    $episode->id
                 ) ?>"><?= lang('Episode.edit') ?></a>
                 <a href="<?= route_to(
                     'episode',
@@ -43,21 +51,15 @@
 ) ?></a>
                 <a href="<?= route_to(
                     'episode_delete',
-                    $podcast->name,
-                    $episode->slug
+                    $podcast->id,
+                    $episode->id
                 ) ?>" class="inline-flex px-4 py-2 text-white bg-red-700 hover:bg-red-800"><?= lang(
     'Episode.delete'
 ) ?></a>
             </article>
         <?php endforeach; ?>
     <?php else: ?>
-        <div class="flex items-center">
-            <p class="mr-4 italic"><?= lang('Podcast.no_episode') ?></p>
-            <a class="self-start px-4 py-2 border hover:bg-gray-100" href="<?= route_to(
-                'episode_create',
-                $podcast->name
-            ) ?>"><?= lang('Episode.create_one') ?></a>
-        </div>
+        <p class="italic"><?= lang('Podcast.no_episode') ?></p>
     <?php endif; ?>
 </div>
 
diff --git a/app/Views/admin/my_account/change_password.php b/app/Views/admin/my_account/change_password.php
index 28cc35eaaa62c0e651837f3eb3910088c1b17925..619085d8261b35e71083cfe96a13389a21b8fef3 100644
--- a/app/Views/admin/my_account/change_password.php
+++ b/app/Views/admin/my_account/change_password.php
@@ -1,8 +1,11 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('MyAccount.changePassword') ?>
+<?= $this->endSection() ?>
+
 
-<h1 class="mb-6 text-xl"><?= lang('MyAccount.changePassword') ?></h1>
+<?= $this->section('content') ?>
 
 <form action="<?= route_to(
     'myAccount_changePassword'
diff --git a/app/Views/admin/my_account/view.php b/app/Views/admin/my_account/view.php
index 03deb336a2084987ce88d69eb0a41123567f6248..92ce2ed5b330879f9a9c67e1b68ba56b76970bdf 100644
--- a/app/Views/admin/my_account/view.php
+++ b/app/Views/admin/my_account/view.php
@@ -1,5 +1,10 @@
 <?= $this->extend('admin/_layout') ?>
 
+<?= $this->section('title') ?>
+<?= lang('MyAccount.info') ?>
+<?= $this->endSection() ?>
+
+
 <?= $this->section('content') ?>
 
 <div class="px-4 py-5 bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php
index c9eed74eb1b8e47b54151ceeb22a3344230ce216..399fd2900c9aa38495711ff676640d10396b64c3 100644
--- a/app/Views/admin/podcast/create.php
+++ b/app/Views/admin/podcast/create.php
@@ -1,14 +1,13 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('Podcast.create') ?>
+<?= $this->endSection() ?>
 
-<h1 class="mb-6 text-xl"><?= lang('Podcast.create') ?></h1>
 
-<div class="mb-8">
-     <?= \Config\Services::validation()->listErrors() ?>
-</div>
+<?= $this->section('content') ?>
 
-<?= form_open_multipart(base_url(route_to('podcast_create')), [
+<?= form_open_multipart(route_to('podcast_create'), [
     'method' => 'post',
     'class' => 'flex flex-col max-w-md',
 ]) ?>
@@ -16,24 +15,32 @@
 
 <div class="flex flex-col mb-4">
     <label for="title"><?= lang('Podcast.form.title') ?></label>
-    <input type="text" class="form-input" id="title" name="title" required />
+    <input type="text" class="form-input" id="title" name="title" value="<?= old(
+        'title'
+    ) ?>" required />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="name"><?= lang('Podcast.form.name') ?></label>
-    <input type="text" class="form-input" id="name" name="name" required />
+    <input type="text" class="form-input" id="name" name="name" value="<?= old(
+        'name'
+    ) ?>" required />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="description"><?= lang('Podcast.form.description') ?></label>
-    <textarea class="form-textarea" id="description" name="description" required></textarea>
+    <textarea class="form-textarea" id="description" name="description" required><?= old(
+        'description'
+    ) ?></textarea>
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="episode_description_footer"><?= lang(
         'Podcast.form.episode_description_footer'
     ) ?></label>
-    <textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"></textarea>
+    <textarea class="form-textarea" id="episode_description_footer" name="episode_description_footer"><?= old(
+        'episode_description_footer'
+    ) ?></textarea>
 </div>
 
 <div class="flex flex-col mb-4">
@@ -45,9 +52,15 @@
     <label for="language"><?= lang('Podcast.form.language') ?></label>
     <select id="language" name="language" autocomplete="off" class="form-select" required>
         <?php foreach ($languages as $language): ?>
-            <option <?= $language->code == $browser_lang
-                ? "selected='selected'"
-                : '' ?> value="<?= $language->code ?>">
+            <option value="<?= $language->code ?>"
+            <?php if (
+                old('language') == $language->code
+            ): ?> selected <?php endif; ?>
+            <?php if (
+                !old('language') &&
+                $language->code == $browser_lang
+            ): ?> selected <?php endif; ?>
+                >
                 <?= $language->native_name ?>
             </option>
         <?php endforeach; ?>
@@ -58,16 +71,20 @@
     <label for="category"><?= lang('Podcast.form.category') ?></label>
     <select id="category" name="category" class="form-select" required>
         <?php foreach ($categories as $category): ?>
-            <option value="<?= $category->code ?>"><?= lang(
-    'Podcast.category_options.' . $category->code
-) ?>
+            <option value="<?= $category->code ?>"
+            <?php if (
+                old('category') == $category->code
+            ): ?> selected <?php endif; ?>
+            ><?= lang('Podcast.category_options.' . $category->code) ?>
             </option>
         <?php endforeach; ?>
     </select>
 </div>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" />
+    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?php if (
+        old('explicit')
+    ): ?> checked <?php endif; ?> />
     <label for="explicit" class="pl-2"><?= lang(
         'Podcast.form.explicit'
     ) ?></label>
@@ -75,48 +92,67 @@
 
 <div class="flex flex-col mb-4">
     <label for="author_name"><?= lang('Podcast.form.author_name') ?></label>
-    <input type="text" class="form-input" id="author_name" name="author_name" />
+    <input type="text" class="form-input" id="author_name" name="author_name" value="<?= old(
+        'author_name'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="author_email"><?= lang('Podcast.form.author_email') ?></label>
-    <input type="email" class="form-input" id="author_email" name="author_email" />
+    <input type="email" class="form-input" id="author_email" name="author_email" value="<?= old(
+        'author_email'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="owner_name"><?= lang('Podcast.form.owner_name') ?></label>
-    <input type="text" class="form-input" id="owner_name" name="owner_name" />
+    <input type="text" class="form-input" id="owner_name" name="owner_name" value="<?= old(
+        'owner_name'
+    ) ?>" />
 </div>
 
 <div class="flex flex-col mb-4">
     <label for="owner_email"><?= lang('Podcast.form.owner_email') ?></label>
-    <input type="email" class="form-input" id="owner_email" name="owner_email" required />
+    <input type="email" class="form-input" id="owner_email" name="owner_email" value="<?= old(
+        'owner_email'
+    ) ?>" required />
 </div>
 
 <fieldset class="flex flex-col mb-4">
     <legend><?= lang('Podcast.form.type.label') ?></legend>
     <label for="episodic" class="inline-flex items-center">
-        <input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required checked />
-        <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>  
+        <input type="radio" class="form-radio" value="episodic" id="episodic" name="type" required <?php if (
+            !old('type') ||
+            old('type') == 'episodic'
+        ): ?> checked <?php endif; ?> />
+        <span class="ml-2"><?= lang('Podcast.form.type.episodic') ?></span>
     </label>
     <label for="serial" class="inline-flex items-center">
-        <input type="radio" class="form-radio" value="serial" id="serial" name="type" required />
-        <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>  
+        <input type="radio" class="form-radio" value="serial" id="serial" name="type" required <?php if (
+            old('type') == 'serial'
+        ): ?> checked <?php endif; ?>  />
+        <span class="ml-2"><?= lang('Podcast.form.type.serial') ?></span>
     </label>
 </fieldset>
 
 <div class="flex flex-col mb-4">
     <label for="copyright"><?= lang('Podcast.form.copyright') ?></label>
-    <input type="text" class="form-input" id="copyright" name="copyright" />
+    <input type="text" class="form-input" id="copyright" name="copyright" value="<?= old(
+        'copyright'
+    ) ?>" />
 </div>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="block" name="block" class="form-checkbox" />
+    <input type="checkbox" id="block" name="block" class="form-checkbox" <?php if (
+        old('block')
+    ): ?> checked <?php endif; ?> />
     <label for="block" class="pl-2"><?= lang('Podcast.form.block') ?></label>
 </div>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="complete" name="complete" class="form-checkbox" />
+    <input type="checkbox" id="complete" name="complete" class="form-checkbox" <?php if (
+        old('complete')
+    ): ?> checked <?php endif; ?> />
     <label for="complete" class="pl-2"><?= lang(
         'Podcast.form.complete'
     ) ?></label>
diff --git a/app/Views/admin/podcast/edit.php b/app/Views/admin/podcast/edit.php
index 4dcfff666b85ca716f4d0902722e092e93216e02..653d2482e7c2b0847ac55c113b9fde53b785747a 100644
--- a/app/Views/admin/podcast/edit.php
+++ b/app/Views/admin/podcast/edit.php
@@ -1,14 +1,13 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('Podcast.edit') ?>
+<?= $this->endSection() ?>
 
-<h1 class="mb-6 text-xl"><?= lang('Podcast.edit') ?></h1>
 
-<div class="mb-8">
-     <?= \Config\Services::validation()->listErrors() ?>
-</div>
+<?= $this->section('content') ?>
 
-<?= form_open_multipart(base_url(route_to('podcast_edit', $podcast->name)), [
+<?= form_open_multipart(route_to('podcast_edit', $podcast->id), [
     'method' => 'post',
     'class' => 'flex flex-col max-w-md',
 ]) ?>
@@ -70,7 +69,9 @@
 </div>
 
 <div class="inline-flex items-center mb-4">
-    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" checked="<?= $podcast->explicit ?>" />
+    <input type="checkbox" id="explicit" name="explicit" class="form-checkbox" <?= $podcast->explicit
+        ? 'checked'
+        : '' ?> />
     <label for="explicit" class="pl-2"><?= lang(
         'Podcast.form.explicit'
     ) ?></label>
@@ -123,7 +124,7 @@
 
 <div class="inline-flex items-center mb-4">
     <input type="checkbox" id="complete" name="complete" class="form-checkbox"
-    <?= $podcast->block ? 'checked' : '' ?> />
+    <?= $podcast->complete ? 'checked' : '' ?> />
     <label for="complete" class="pl-2"><?= lang(
         'Podcast.form.complete'
     ) ?></label>
diff --git a/app/Views/admin/podcast/list.php b/app/Views/admin/podcast/list.php
index 7510eeef26b29c37cb52cf50932e3f512fb29198..c3fd22cd91094b151ef3be99722b86c9ad6bacb7 100644
--- a/app/Views/admin/podcast/list.php
+++ b/app/Views/admin/podcast/list.php
@@ -1,10 +1,15 @@
 <?= $this->extend('admin/_layout') ?>
 
+<?= $this->section('title') ?>
+<?= lang('Podcast.all_podcasts') ?> (<?= count($all_podcasts) ?>)
+<?= $this->endSection() ?>
+
+
 <?= $this->section('content') ?>
 
-<h1 class="mb-2 text-xl"><?= lang('Podcast.all_podcasts') ?> (<?= count(
-     $all_podcasts
- ) ?>)</h1>
+<a class="inline-block px-4 py-2 mb-2 border hover:bg-gray-100" href="<?= route_to(
+    'podcast_create'
+) ?>"><?= lang('Podcast.create') ?></a>
 <div class="flex flex-wrap">
     <?php if ($all_podcasts): ?>
         <?php foreach ($all_podcasts as $podcast): ?>
@@ -12,36 +17,36 @@
                 <img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
                 <a href="<?= route_to(
                     'episode_list',
-                    $podcast->name
+                    $podcast->id
                 ) ?>" class="hover:underline">
                     <h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
                 </a>
                 <p class="mb-4 text-gray-600">@<?= $podcast->name ?></p>
                 <a class="inline-flex px-2 py-1 mb-2 text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
                     'podcast_edit',
-                    $podcast->name
+                    $podcast->id
                 ) ?>"><?= lang('Podcast.edit') ?></a>
                 <a class="inline-flex px-2 py-1 mb-2 text-white bg-indigo-700 hover:bg-indigo-800" href="<?= route_to(
                     'episode_list',
-                    $podcast->name
+                    $podcast->id
                 ) ?>"><?= lang('Podcast.see_episodes') ?></a>
+                <a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to(
+                    'contributor_list',
+                    $podcast->id
+                ) ?>"><?= lang('Podcast.see_contributors') ?></a>
                 <a class="inline-flex px-2 py-1 text-white bg-gray-700 hover:bg-gray-800" href="<?= route_to(
                     'podcast',
                     $podcast->name
                 ) ?>"><?= lang('Podcast.goto_page') ?></a>
                 <a class="inline-flex px-2 py-1 text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
                     'podcast_delete',
-                    $podcast->name
+                    $podcast->id
                 ) ?>"><?= lang('Podcast.delete') ?></a>
+
             </article>
         <?php endforeach; ?>
     <?php else: ?>
-        <div class="flex items-center">
-            <p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p>
-            <a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to(
-                'podcast_create'
-            ) ?>"><?= lang('Podcast.create_one') ?></a>
-        </div>
+        <p class="italic"><?= lang('Podcast.no_podcast') ?></p>
     <?php endif; ?>
 </div>
 
diff --git a/app/Views/admin/user/create.php b/app/Views/admin/user/create.php
index 82bfe99d2547137b33a8770f5cff1bfdf87d5b2d..865521ce52edfcaae6e9c4bbf7f6d7f4b6320e02 100644
--- a/app/Views/admin/user/create.php
+++ b/app/Views/admin/user/create.php
@@ -1,12 +1,11 @@
 <?= $this->extend('admin/_layout') ?>
 
-<?= $this->section('content') ?>
+<?= $this->section('title') ?>
+<?= lang('User.create') ?>
+<?= $this->endSection() ?>
 
-<h1 class="mb-6 text-xl"><?= lang('User.create') ?></h1>
 
-<div class="mb-8">
-     <?= \Config\Services::validation()->listErrors() ?>
-</div>
+<?= $this->section('content') ?>
 
 <form action="<?= route_to(
     'user_create'
diff --git a/app/Views/admin/user/list.php b/app/Views/admin/user/list.php
index a0d121213b8e6b697fe1f274abe57c77a07512ac..b41502250d700da89d6408f6f6e0e4702b9027e2 100644
--- a/app/Views/admin/user/list.php
+++ b/app/Views/admin/user/list.php
@@ -1,61 +1,55 @@
 <?= $this->extend('admin/_layout') ?>
 
+<?= $this->section('title') ?>
+<?= lang('User.all_users') ?> (<?= count($all_users) ?>)
+<?= $this->endSection() ?>
+
+
 <?= $this->section('content') ?>
 
-<div class="flex flex-wrap">
-    <?php if ($all_users): ?>
-        <table class="table-auto">
-            <thead>
-                <tr>
-                    <th class="px-4 py-2">Username</th>
-                    <th class="px-4 py-2">Email</th>
-                    <th class="px-4 py-2">Permissions</th>
-                    <th class="px-4 py-2">Banned?</th>
-                    <th class="px-4 py-2">Actions</th>
-                </tr>
-            </thead>
-            <tbody>
-                <?php foreach ($all_users as $user): ?>
-                <tr>
-                    <td class="px-4 py-2 border"><?= $user->username ?></td>
-                    <td class="px-4 py-2 border"><?= $user->email ?></td>
-                    <td class="px-4 py-2 border">[<?= implode(
-                        ', ',
-                        $user->permissions
-                    ) ?>]</td>
-                    <td class="px-4 py-2 border"><?= $user->isBanned()
-                        ? 'Yes'
-                        : 'No' ?></td>
-                    <td class="px-4 py-2 border">
-                        <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
-                            'user_force_pass_reset',
-                            $user->username
-                        ) ?>"><?= lang('User.forcePassReset') ?></a>
-                        <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
-                            $user->isBanned() ? 'user_unban' : 'user_ban',
-                            $user->username
-                        ) ?>">
-                        <?= $user->isBanned()
-                            ? lang('User.unban')
-                            : lang('User.ban') ?></a>
-                        <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
-                            'user_delete',
-                            $user->username
-                        ) ?>"><?= lang('User.delete') ?></a>
-                    </td>
-                </tr>
-                <?php endforeach; ?>
-            </tbody>
-        </table>
-    <?php else: ?>
-        <div class="flex items-center">
-            <p class="mr-4 italic"><?= lang('Podcast.no_podcast') ?></p>
-            <a class="self-start px-4 py-2 border hover:bg-gray-100 " href="<?= route_to(
-                'podcast_create'
-            ) ?>"><?= lang('Podcast.create_one') ?></a>
-        </div>
-    <?php endif; ?>
-</div>
+<table class="table-auto">
+    <thead>
+        <tr>
+            <th class="px-4 py-2">Username</th>
+            <th class="px-4 py-2">Email</th>
+            <th class="px-4 py-2">Permissions</th>
+            <th class="px-4 py-2">Banned?</th>
+            <th class="px-4 py-2">Actions</th>
+        </tr>
+    </thead>
+    <tbody>
+        <?php foreach ($all_users as $user): ?>
+        <tr>
+            <td class="px-4 py-2 border"><?= $user->username ?></td>
+            <td class="px-4 py-2 border"><?= $user->email ?></td>
+            <td class="px-4 py-2 border">[<?= implode(
+                ', ',
+                $user->permissions
+            ) ?>]</td>
+            <td class="px-4 py-2 border"><?= $user->isBanned()
+                ? 'Yes'
+                : 'No' ?></td>
+            <td class="px-4 py-2 border">
+                <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-teal-700 hover:bg-teal-800" href="<?= route_to(
+                    'user_force_pass_reset',
+                    $user->id
+                ) ?>"><?= lang('User.forcePassReset') ?></a>
+                <a class="inline-flex px-2 py-1 mb-2 text-sm text-white bg-orange-700 hover:bg-orange-800" href="<?= route_to(
+                    $user->isBanned() ? 'user_unban' : 'user_ban',
+                    $user->id
+                ) ?>">
+                <?= $user->isBanned()
+                    ? lang('User.unban')
+                    : lang('User.ban') ?></a>
+                <a class="inline-flex px-2 py-1 text-sm text-white bg-red-700 hover:bg-red-800" href="<?= route_to(
+                    'user_delete',
+                    $user->id
+                ) ?>"><?= lang('User.delete') ?></a>
+            </td>
+        </tr>
+        <?php endforeach; ?>
+    </tbody>
+</table>
 
 <?= $this->endSection()
 ?>
diff --git a/app/Views/auth/_layout.php b/app/Views/auth/_layout.php
index 24d4aff6b3303464db23dba4672749c53ce3460a..80f4c4ecb7e80a19f3141edfa603bc8ffac6c950 100644
--- a/app/Views/auth/_layout.php
+++ b/app/Views/auth/_layout.php
@@ -17,9 +17,7 @@
 ) ?></a>
 	</header>
 	<main class="w-full max-w-md px-6 py-4 mx-auto bg-white rounded-lg shadow">
-		<div class="mb-4">
-			<?= view('_message_block') ?>
-		</div>
+		<?= view('_message_block') ?>
 		<?= $this->renderSection('content') ?>
 	</main>
 	<footer class="flex flex-col text-sm">
diff --git a/composer.json b/composer.json
index 3c0f646cd0026f0e03ed50e1ebfcb56a1387d852..ae8d398d9ac9549b48e1b165a72bd1db476a3644 100644
--- a/composer.json
+++ b/composer.json
@@ -6,7 +6,7 @@
   "license": "AGPL-3.0-or-later",
   "require": {
     "php": ">=7.2",
-    "codeigniter4/framework": "^4",
+    "codeigniter4/framework": "4.0.3",
     "james-heinrich/getid3": "~2.0.0-dev",
     "whichbrowser/parser": "^2.0",
     "geoip2/geoip2": "~2.0",
diff --git a/composer.lock b/composer.lock
index 16d8fd9a7ca1516163dac486b421b9d302a82c49..93f15e074ab82f13a3ff4589914436e0357f0439 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a03d5be6665057254fa301cada96586e",
+    "content-hash": "e494a281a4c6a239790ea930d05764e2",
     "packages": [
         {
             "name": "codeigniter4/framework",
@@ -1158,33 +1158,33 @@
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.10.3",
+            "version": "1.11.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "451c3cd1418cf640de218914901e51b064abb093"
+                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
-                "reference": "451c3cd1418cf640de218914901e51b064abb093",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
+                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.0.2",
-                "php": "^5.3|^7.0",
-                "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
-                "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
-                "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
+                "doctrine/instantiator": "^1.2",
+                "php": "^7.2",
+                "phpdocumentor/reflection-docblock": "^5.0",
+                "sebastian/comparator": "^3.0 || ^4.0",
+                "sebastian/recursion-context": "^3.0 || ^4.0"
             },
             "require-dev": {
-                "phpspec/phpspec": "^2.5 || ^3.2",
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+                "phpspec/phpspec": "^6.0",
+                "phpunit/phpunit": "^8.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.10.x-dev"
+                    "dev-master": "1.11.x-dev"
                 }
             },
             "autoload": {
@@ -1217,7 +1217,7 @@
                 "spy",
                 "stub"
             ],
-            "time": "2020-03-05T15:02:03+00:00"
+            "time": "2020-07-08T12:44:21+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -2181,16 +2181,16 @@
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.17.1",
+            "version": "v1.18.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
+                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
-                "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
+                "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
                 "shasum": ""
             },
             "require": {
@@ -2202,7 +2202,7 @@
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.17-dev"
+                    "dev-master": "1.18-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
@@ -2253,27 +2253,27 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2020-06-06T08:46:27+00:00"
+            "time": "2020-07-14T12:35:20+00:00"
         },
         {
             "name": "theseer/tokenizer",
-            "version": "1.1.3",
+            "version": "1.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
+                "reference": "75a63c33a8577608444246075ea0af0d052e452a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
-                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
+                "reference": "75a63c33a8577608444246075ea0af0d052e452a",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-tokenizer": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.0"
+                "php": "^7.2 || ^8.0"
             },
             "type": "library",
             "autoload": {
@@ -2293,24 +2293,30 @@
                 }
             ],
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
-            "time": "2019-06-13T22:48:21+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-07-12T23:59:07+00:00"
         },
         {
             "name": "webmozart/assert",
-            "version": "1.9.0",
+            "version": "1.9.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "9dc4f203e36f2b486149058bade43c851dd97451"
+                "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451",
-                "reference": "9dc4f203e36f2b486149058bade43c851dd97451",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+                "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0",
+                "php": "^5.3.3 || ^7.0 || ^8.0",
                 "symfony/polyfill-ctype": "^1.8"
             },
             "conflict": {
@@ -2342,7 +2348,7 @@
                 "check",
                 "validate"
             ],
-            "time": "2020-06-16T10:16:42+00:00"
+            "time": "2020-07-08T17:02:28+00:00"
         }
     ],
     "aliases": [],