refactor(auth): replace myth/auth with codeigniter/shield + define new roles

closes #222
This commit is contained in:
Yassine Doghri 2022-10-15 11:22:08 +00:00
parent c760acc79d
commit c1287cbe6c
213 changed files with 3372 additions and 3210 deletions

View File

@ -29,6 +29,7 @@
"bmewburn.vscode-intelephense-client",
"bradlc.vscode-tailwindcss",
"breezelin.phpstan",
"DavidAnson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
@ -41,6 +42,7 @@
"runem.lit-plugin",
"streetsidesoftware.code-spell-checker",
"stylelint.vscode-stylelint",
"wayou.vscode-todo-highlight"
"wayou.vscode-todo-highlight",
"yzhang.markdown-all-in-one"
]
}

View File

@ -29,6 +29,10 @@ if (! function_exists('view')) {
*/
function view(string $name, array $data = [], array $options = []): string
{
if (array_key_exists('theme', $options)) {
Theme::setTheme($options['theme']);
}
$path = Theme::path();
/** @var CodeIgniter\View\View $renderer */
@ -55,6 +59,8 @@ if (! function_exists('lang')) {
*
* @param array<int|string, string> $args
*
* TODO: remove, and escape args when necessary
*
* @return string|string[]
*/
function lang(string $line, array $args = [], ?string $locale = null, bool $escape = true): string | array

View File

@ -77,7 +77,7 @@ class Email extends BaseConfig
/**
* Type of mail, either 'text' or 'html'
*/
public string $mailType = 'text';
public string $mailType = 'html';
/**
* Character set (utf-8, iso-8859-1, etc.)

View File

@ -9,7 +9,6 @@ use App\Entities\Post;
use App\Models\EpisodeModel;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
use Modules\Auth\Entities\User;
/*
* --------------------------------------------------------------------
@ -56,21 +55,6 @@ Events::on('pre_system', static function () {
}
});
Events::on('login', static function (User $user): void {
helper('auth');
// set interact_as_actor_id value
$userPodcasts = $user->podcasts;
if ($userPodcasts = $user->podcasts) {
set_interact_as_actor($userPodcasts[0]->actor_id);
}
});
Events::on('logout', static function (User $user): void {
helper('auth');
// remove user's interact_as_actor session
remove_interact_as_actor();
});
/*
* --------------------------------------------------------------------
* Fediverse events

View File

@ -15,8 +15,6 @@ use Modules\Auth\Filters\PermissionFilter;
use Modules\Fediverse\Filters\AllowCorsFilter;
use Modules\Fediverse\Filters\FediverseFilter;
use Modules\PremiumPodcasts\Filters\PodcastUnlockFilter;
use Myth\Auth\Filters\LoginFilter;
use Myth\Auth\Filters\RoleFilter;
class Filters extends BaseConfig
{
@ -31,8 +29,6 @@ class Filters extends BaseConfig
'honeypot' => Honeypot::class,
'invalidchars' => InvalidChars::class,
'secureheaders' => SecureHeaders::class,
'login' => LoginFilter::class,
'role' => RoleFilter::class,
'permission' => PermissionFilter::class,
'fediverse' => FediverseFilter::class,
'allow-cors' => AllowCorsFilter::class,
@ -86,7 +82,7 @@ class Filters extends BaseConfig
parent::__construct();
$this->filters = [
'login' => [
'session' => [
'before' => [config('Admin')->gateway . '*', config('Analytics')->gateway . '*'],
],
'podcast-unlock' => [

View File

@ -214,7 +214,7 @@ $routes->get('/pages/(:slug)', 'PageController/$1', [
$routes->group('@(:podcastHandle)', static function ($routes): void {
$routes->post('posts/new', 'PostController::attemptCreate/$1', [
'as' => 'post-attempt-create',
'filter' => 'permission:podcast-manage_publications',
'filter' => 'permission:podcast#.manage-publications',
]);
// Post
$routes->group('posts/(:uuid)', static function ($routes): void {
@ -251,14 +251,14 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
// Actions
$routes->post('action', 'PostController::attemptAction/$1/$2', [
'as' => 'post-attempt-action',
'filter' => 'permission:podcast-interact_as',
'filter' => 'permission:podcast#.interact-as',
]);
$routes->post(
'block-actor',
'PostController::attemptBlockActor/$1/$2',
[
'as' => 'post-attempt-block-actor',
'filter' => 'permission:fediverse-block_actors',
'filter' => 'permission:fediverse.manage-blocks',
],
);
$routes->post(
@ -266,12 +266,12 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
'PostController::attemptBlockDomain/$1/$2',
[
'as' => 'post-attempt-block-domain',
'filter' => 'permission:fediverse-block_domains',
'filter' => 'permission:fediverse.manage-blocks',
],
);
$routes->post('delete', 'PostController::attemptDelete/$1/$2', [
'as' => 'post-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
'filter' => 'permission:podcast#.manage-publications',
]);
$routes->get(
'remote/(:postAction)',

View File

@ -17,7 +17,7 @@ class Security extends BaseConfig
*
* @var 'cookie'|'session'
*/
public string $csrfProtection = 'cookie';
public string $csrfProtection = 'session';
/**
* --------------------------------------------------------------------------

View File

@ -11,7 +11,6 @@ use CodeIgniter\Validation\CreditCardRules;
use CodeIgniter\Validation\FileRules;
use CodeIgniter\Validation\FormatRules;
use CodeIgniter\Validation\Rules;
use Myth\Auth\Authentication\Passwords\ValidationRules as PasswordRules;
class Validation extends BaseConfig
{
@ -27,7 +26,6 @@ class Validation extends BaseConfig
CreditCardRules::class,
AppRules::class,
AppFileRules::class,
PasswordRules::class,
];
/**

View File

@ -20,12 +20,12 @@ class ActorController extends FediverseActorController
/**
* @var string[]
*/
protected $helpers = ['auth', 'svg', 'components', 'misc', 'seo'];
protected $helpers = ['svg', 'components', 'misc', 'seo'];
public function follow(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
// @phpstan-ignore-next-line
$this->registerPodcastWebpageHit($this->actor->podcast->id);
}

View File

@ -34,5 +34,7 @@ abstract class BaseController extends Controller
parent::initController($request, $response, $logger);
Theme::setTheme('app');
$this->helpers = array_merge($this->helpers, ['setting']);
}
}

View File

@ -23,7 +23,7 @@ class CreditsController extends BaseController
$cacheName = implode(
'_',
array_filter(['page', 'credits', $locale, can_user_interact() ? 'authenticated' : null]),
array_filter(['page', 'credits', $locale, auth()->loggedIn() ? 'authenticated' : null]),
);
if (! ($found = cache($cacheName))) {

View File

@ -79,7 +79,7 @@ class EpisodeCommentController extends BaseController
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
@ -91,7 +91,8 @@ class EpisodeCommentController extends BaseController
"comment#{$this->comment->id}",
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -105,7 +106,7 @@ class EpisodeCommentController extends BaseController
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/comment', $data);
}

View File

@ -66,7 +66,7 @@ class EpisodeController extends BaseController
public function index(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
@ -79,7 +79,8 @@ class EpisodeController extends BaseController
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -94,7 +95,7 @@ class EpisodeController extends BaseController
$this->podcast->id,
);
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/comments', $data);
@ -115,7 +116,7 @@ class EpisodeController extends BaseController
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
@ -129,7 +130,8 @@ class EpisodeController extends BaseController
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -144,7 +146,7 @@ class EpisodeController extends BaseController
$this->podcast->id,
);
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/activity', $data);
@ -167,7 +169,7 @@ class EpisodeController extends BaseController
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}

View File

@ -13,13 +13,20 @@ namespace App\Controllers;
use App\Models\PodcastModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use Exception;
class HomeController extends BaseController
{
public function index(): RedirectResponse | string
{
$db = db_connect();
if ($db->getDatabase() === '' || ! $db->tableExists('podcasts')) {
$sortOptions = ['activity', 'created_desc', 'created_asc'];
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
'sort'
) : 'activity';
try {
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
} catch (Exception) {
// Database connection has not been set or could not find the podcasts table
// Redirecting to install page because it is likely that Castopod has not been installed yet.
// NB: as base_url wouldn't have been defined here, redirect to install wizard manually
@ -27,13 +34,6 @@ class HomeController extends BaseController
return redirect()->to(rtrim(host_url(), '/') . $route);
}
$sortOptions = ['activity', 'created_desc', 'created_asc'];
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
'sort'
) : 'activity';
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
// check if there's only one podcast to redirect user to it
if (count($allPodcasts) === 1) {
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);

View File

@ -24,7 +24,8 @@ class MapController extends BaseController
'map',
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);

View File

@ -44,7 +44,8 @@ class PageController extends BaseController
$this->page->slug,
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);

View File

@ -62,7 +62,7 @@ class PodcastController extends BaseController
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
@ -75,7 +75,8 @@ class PodcastController extends BaseController
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -87,7 +88,7 @@ class PodcastController extends BaseController
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/activity', $data);
@ -111,7 +112,7 @@ class PodcastController extends BaseController
public function about(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
@ -124,7 +125,8 @@ class PodcastController extends BaseController
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -138,7 +140,7 @@ class PodcastController extends BaseController
];
// // if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/about', $data);
@ -162,7 +164,7 @@ class PodcastController extends BaseController
public function episodes(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
@ -191,7 +193,8 @@ class PodcastController extends BaseController
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -264,7 +267,7 @@ class PodcastController extends BaseController
),
];
if (can_user_interact()) {
if (auth()->loggedIn()) {
return view('podcast/episodes', $data);
}

View File

@ -70,7 +70,7 @@ class PostController extends FediversePostController
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
@ -85,7 +85,8 @@ class PostController extends FediversePostController
"post#{$this->post->id}",
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
@ -97,7 +98,7 @@ class PostController extends FediversePostController
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('post/post', $data);
}
@ -239,7 +240,7 @@ class PostController extends FediversePostController
public function remoteAction(string $action): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
if (! auth()->loggedIn()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}

View File

@ -55,10 +55,12 @@ class AddMedia extends Migration
],
'uploaded_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'uploaded_at' => [

View File

@ -18,7 +18,6 @@ class AppSeeder extends Seeder
{
public function run(): void
{
$this->call('AuthSeeder');
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('PlatformSeeder');

View File

@ -1,328 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PermissionSeeder Inserts permissions
*
* @copyright 2020 Ad Aures
* @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
{
/**
* @var array<string, string>[]
*/
protected array $groups = [
[
'name' => 'superadmin',
'description' =>
'Somebody who has access to all the castopod instance features',
],
[
'name' => 'podcast_admin',
'description' =>
'Somebody who has access to all the features within a given podcast',
],
];
/**
* Build permissions array as a list of:
*
* ``` context => [ [action, description], [action, description], ... ] ```
*
* @var array<string, array<string, string|string[]>[]>
*/
protected array $permissions = [
'settings' => [
[
'name' => 'view',
'description' => 'View settings options',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage',
'description' => 'Update general settings',
'has_permission' => ['superadmin'],
],
],
'users' => [
[
'name' => 'create',
'description' => 'Create a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all users',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any user info',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_authorizations',
'description' => 'Add or remove roles/permissions to a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_bans',
'description' => 'Ban / unban a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'force_pass_reset',
'description' =>
'Force a user to update his password upon next login',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete user without removing him from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of a user from the database',
'has_permission' => ['superadmin'],
],
],
'pages' => [
[
'name' => 'manage',
'description' => 'List / create / edit / delete pages',
'has_permission' => ['superadmin'],
],
],
'podcasts' => [
[
'name' => 'create',
'description' => 'Add a new podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'import',
'description' => 'Import a new podcast from an external feed',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all podcasts and their episodes',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any podcast and their contributors list',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' => 'Delete any podcast from the database',
'has_permission' => ['superadmin'],
],
],
'episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of any podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any episode of any podcast',
'has_permission' => ['superadmin'],
],
],
'podcast' => [
[
'name' => 'view',
'description' => 'View a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_subscriptions',
'description' =>
'Add / edit / remove podcast subscriptions',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_contributors',
'description' =>
'Add / remove contributors to a podcast and edit their roles',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_platforms',
'description' => 'Set / remove platform links of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_publications',
'description' =>
'Publish a podcast and publish / unpublish its episodes & posts',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'description' =>
'Interact as the podcast to favourite / share or reply to posts.',
'has_permission' => ['podcast_admin'],
],
],
'podcast_episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'view',
'description' => 'View any episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'create',
'description' => 'Add new episodes for a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit an episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete',
'description' =>
'Delete all occurrences of an episode of a podcast from the database',
'has_permission' => ['podcast_admin'],
],
],
'person' => [
[
'name' => 'create',
'description' => 'Add a new person',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all persons',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any person',
'has_permission' => ['superadmin'],
],
[
'name' => 'edit',
'description' => 'Edit a person',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete permanently any person from the database',
'has_permission' => ['superadmin'],
],
],
'fediverse' => [
[
'name' => 'block_actors',
'description' =>
'Block fediverse actors from interacting with the instance.',
'has_permission' => ['superadmin'],
],
[
'name' => 'block_domains',
'description' =>
'Block fediverse domains from interacting with the instance.',
'has_permission' => ['superadmin'],
],
],
];
public function run(): void
{
$groupId = 0;
$dataGroups = [];
foreach ($this->groups as $group) {
$dataGroups[] = [
'id' => ++$groupId,
'name' => $group['name'],
'description' => $group['description'],
];
}
// Map permissions to a format the `auth_permissions` table expects
$dataPermissions = [];
$dataGroupsPermissions = [];
$permissionId = 0;
foreach ($this->permissions as $context => $actions) {
foreach ($actions as $action) {
$dataPermissions[] = [
'id' => ++$permissionId,
'name' => $context . '-' . $action['name'],
'description' => $action['description'],
];
foreach ($action['has_permission'] as $role) {
// link permission to specified groups
$dataGroupsPermissions[] = [
'group_id' => $this->getGroupIdByName($role, $dataGroups),
'permission_id' => $permissionId,
];
}
}
}
if ($this->db->table('auth_groups')->countAll() < count($dataPermissions)) {
$this->db
->table('auth_permissions')
->ignore(true)
->insertBatch($dataPermissions);
}
if ($this->db->table('auth_groups')->countAll() < count($dataGroups)) {
$this->db
->table('auth_groups')
->ignore(true)
->insertBatch($dataGroups);
}
if ($this->db->table('auth_groups_permissions')->countAll() < count($dataGroupsPermissions)) {
$this->db
->table('auth_groups_permissions')
->ignore(true)
->insertBatch($dataGroupsPermissions);
}
}
/**
* @param array<string, string|int>[] $dataGroups
*/
public static function getGroupIdByName(string $name, array $dataGroups): ?int
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
}

View File

@ -18,24 +18,32 @@ class TestSeeder extends Seeder
{
public function run(): void
{
helper('setting');
/**
* Inserts an active user with the following credentials: username: admin password: AGUehL3P
* Inserts an owner with the following credentials: admin: `admin@example.com` password: `AGUehL3P`
*/
$this->db->table('users')
->insert([
'id' => 1,
'username' => 'admin',
'email' => 'admin@example.com',
'password_hash' =>
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
'is_owner' => 1,
]);
$this->db->table('auth_identities')
->insert([
'id' => 1,
'user_id' => 1,
'type' => 'email_password',
'secret' => 'admin@example.com',
'secret2' => '$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
]);
$this->db
->table('auth_groups_users')
->insert([
'group_id' => 1,
'user_id' => 1,
'group' => setting('AuthGroups.mostPowerfulGroup'),
]);
}
}

View File

@ -17,11 +17,11 @@ use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use App\Models\UserModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\I18n\Time;
use Modules\Auth\Entities\User;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
/**
* @property int $id

View File

@ -18,18 +18,18 @@ use App\Models\EpisodeModel;
use App\Models\MediaModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\UserModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Auth\Entities\User;
use Modules\Auth\Models\UserModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException;
@ -100,6 +100,8 @@ class Podcast extends Entity
{
protected string $link;
protected string $at_handle;
protected ?Actor $actor = null;
protected ?Image $cover = null;
@ -208,6 +210,11 @@ class Podcast extends Entity
'updated_by' => 'integer',
];
public function getAtHandle(): string
{
return '@' . $this->handle;
}
/**
* @noRector ReturnTypeDeclarationRector
*/

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Models\ActorModel;
use Modules\Auth\Entities\User;
use Modules\Fediverse\Entities\Actor;
if (! function_exists('user')) {
/**
* Returns the User instance for the current logged in user.
*/
function user(): ?User
{
$authenticate = service('authentication');
$authenticate->check();
return $authenticate->user();
}
}
if (! function_exists('set_interact_as_actor')) {
/**
* Sets the actor id of which the user is acting as
*/
function set_interact_as_actor(int $actorId): void
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
$session->set('interact_as_actor_id', $actorId);
}
}
if (! function_exists('remove_interact_as_actor')) {
/**
* Removes the actor id of which the user is acting as
*/
function remove_interact_as_actor(): void
{
$session = session();
$session->remove('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor_id')) {
/**
* Sets the podcast id of which the user is acting as
*/
function interact_as_actor_id(): int
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
return $session->get('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor')) {
/**
* Get the actor the user is currently interacting as
*/
function interact_as_actor(): Actor | false
{
$authenticate = service('authentication');
$authenticate->check();
$session = session();
if ($session->has('interact_as_actor_id')) {
return model(ActorModel::class, false)->getActorById($session->get('interact_as_actor_id'));
}
return false;
}
}
if (! function_exists('can_user_interact')) {
function can_user_interact(): bool
{
return (bool) interact_as_actor();
}
}

View File

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace App\Models;
use App\Entities\Podcast;
use CodeIgniter\Database\Query;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Model;
use phpseclib\Crypt\RSA;
@ -205,15 +204,14 @@ class PodcastModel extends Model
/**
* Gets all the podcasts a given user is contributing to
*
* @param string[] $userPodcastIds
* @return Podcast[] podcasts
*/
public function getUserPodcasts(int $userId): array
public function getUserPodcasts(int $userId, array $userPodcastIds): array
{
$cacheName = "user{$userId}_podcasts";
if (! ($found = cache($cacheName))) {
$found = $this->select('podcasts.*')
->join('podcasts_users', 'podcasts_users.podcast_id = podcasts.id')
->where('podcasts_users.user_id', $userId)
$found = $userPodcastIds === [] ? [] : $this->whereIn('id', $userPodcastIds)
->findAll();
cache()
@ -223,76 +221,18 @@ class PodcastModel extends Model
return $found;
}
public function addPodcastContributor(int $userId, int $podcastId, int $groupId): Query | bool
public function getContributorGroup(int $userId, int $podcastId): int | false
{
cache()->delete("podcast#{$podcastId}_contributors");
$data = [
'user_id' => $userId,
'podcast_id' => $podcastId,
'group_id' => $groupId,
];
return $this->db->table('podcasts_users')
->insert($data);
}
public function updatePodcastContributor(int $userId, int $podcastId, int $groupId): bool
{
cache()->delete("podcast#{$podcastId}_contributors");
return $this->db
->table('podcasts_users')
->where([
'user_id' => $userId,
'podcast_id' => $podcastId,
])
->update([
'group_id' => $groupId,
]);
}
public function removePodcastContributor(int $userId, int $podcastId): string | bool
{
cache()->delete("podcast#{$podcastId}_contributors");
return $this->db
->table('podcasts_users')
->where([
'user_id' => $userId,
'podcast_id' => $podcastId,
])
->delete();
}
public function getContributorGroupId(int $userId, int | string $podcastId): int | false
{
if (! is_numeric($podcastId)) {
// identifier is the podcast name, request must be a join
$userPodcast = $this->db
->table('podcasts_users')
->select('group_id, user_id')
->join('podcasts', 'podcasts.id = podcasts_users.podcast_id')
->where([
'user_id' => $userId,
'handle' => $podcastId,
])
->get()
->getResultObject();
} else {
$userPodcast = $this->db
->table('podcasts_users')
->select('group_id')
->where([
'user_id' => $userId,
'podcast_id' => $podcastId,
])
->get()
->getResultObject();
}
$userPodcast = $this->db
->table('auth_groups_users')
->select('user_id, group')
->where('user_id', $userId)
->like('group', "podcast#{$podcastId}")
->get()
->getResultObject();
return $userPodcast !== []
? (int) $userPodcast[0]->group_id
? (int) $userPodcast[0]->group
: false;
}

View File

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use Modules\Auth\Entities\User;
use Myth\Auth\Models\UserModel as MythAuthUserModel;
class UserModel extends MythAuthUserModel
{
/**
* @var string
*/
protected $returnType = User::class;
/**
* @return User[]
*/
public function getPodcastContributors(int $podcastId): array
{
$cacheName = "podcast#{$podcastId}_contributors";
if (! ($found = cache($cacheName))) {
$found = $this->select('users.*, auth_groups.name as podcast_role')
->join('podcasts_users', 'podcasts_users.user_id = users.id')
->join('auth_groups', 'auth_groups.id = podcasts_users.group_id')
->where('podcasts_users.podcast_id', $podcastId)
->findAll();
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
public function getPodcastContributor(int $userId, int $podcastId): ?User
{
// @phpstan-ignore-next-line
return $this->select('users.*, podcasts_users.podcast_id as podcast_id, auth_groups.name as podcast_role')
->join('podcasts_users', 'podcasts_users.user_id = users.id')
->join('auth_groups', 'auth_groups.id = podcasts_users.group_id')
->where([
'users.id' => $userId,
'podcast_id' => $podcastId,
])
->first();
}
}

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M3.783 2.826L12 1l8.217 1.826a1 1 0 0 1 .783.976v9.987a6 6 0 0 1-2.672 4.992L12 23l-6.328-4.219A6 6 0 0 1 3 13.79V3.802a1 1 0 0 1 .783-.976zM12 11a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm-4.473 5h8.946a4.5 4.5 0 0 0-8.946 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 347 B

View File

@ -0,0 +1,29 @@
<?= helper(['components', 'svg']) ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>403 Forbidden</title>
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
<?= service('vite')->asset('styles/index.css', 'css') ?>
</head>
<body class="flex flex-col items-center justify-center min-h-screen px-2 text-center bg-base theme-<?= service('settings')
->get('App.theme') ?>">
<?= svg('castopod-mascot_confused', 'h-64') ?>
<h1 class="mt-4 text-3xl font-bold font-display md:text-4xl lg:text-5xl">403 - Forbidden</h1>
<p class="mb-6 text-lg text-skin-muted md:text-xl lg:text-2xl">
<?php if (isset($message) && $message !== '(null)'): ?>
<?= esc($message) ?>
<?php else: ?>
You do not have sufficient permissions to access that page.
<?php endif; ?>
</p>
<a href="<?= previous_url() ?>" class="inline-flex items-center justify-center px-3 py-1 text-sm font-semibold rounded-full shadow-xs text-accent-contrast focus:ring-accent md:px-4 md:py-2 md:text-base bg-accent-base hover:bg-accent-hover"><?= lang('Common.go_back') ?></a>
</body>
</html>

View File

@ -14,7 +14,7 @@
<body class="flex flex-col items-center justify-center min-h-screen px-2 text-center bg-base theme-<?= service('settings')
->get('App.theme') ?>">
<?= svg('castopod-mascot_confused', 'h-64') ?>
<h1 class="text-3xl font-bold font-display md:text-4xl lg:text-5xl">404 - File Not Found</h1>
<h1 class="mt-4 text-3xl font-bold font-display md:text-4xl lg:text-5xl">404 - File Not Found</h1>
<p class="mb-6 text-lg text-skin-muted md:text-xl lg:text-2xl">
<?php if (isset($message) && $message !== '(null)'): ?>

View File

@ -10,14 +10,14 @@
<title>Whoops!</title>
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
<?= service('vite')->asset('styles/index.css', 'css') ?>
<?php if (service('authentication')->isLoggedIn()): ?>
<?php if (auth()->loggedIn()): ?>
<?= service('vite')->asset('js/error.ts', 'js') ?>
<?php endif; ?>
</head>
<body class="flex flex-col items-center justify-center min-h-screen px-4 bg-base gap-y-12 theme-<?= service('settings')
->get('App.theme') ?>">
<?php if (service('authentication')->isLoggedIn()): ?>
<?php if (auth()->loggedIn()): ?>
<div class="flex flex-col items-center justify-center flex-1 gap-6">
<div class="flex flex-col items-center">
<?= svg('castopod-mascot_confused', 'w-full max-w-xs p-6') ?>

View File

@ -11,7 +11,6 @@
"james-heinrich/getid3": "^2.0.x-dev",
"whichbrowser/parser": "^v2.1.7",
"geoip2/geoip2": "v2.13.0",
"myth/auth": "dev-develop",
"league/commonmark": "^2.3.5",
"vlucas/phpdotenv": "^v5.4.1",
"league/html-to-markdown": "^v5.1.0",
@ -23,7 +22,8 @@
"essence/essence": "^3.5.4",
"codeigniter4/settings": "^v2.1.0",
"chrisjean/php-ico": "^1.0.4",
"melbahja/seo": "^v2.1.1"
"melbahja/seo": "^v2.1.1",
"codeigniter4/shield": "dev-develop"
},
"require-dev": {
"mikey179/vfsstream": "^v1.6.11",

137
composer.lock generated
View File

@ -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": "caa3b9ff10584fe03c7be1176713b427",
"content-hash": "51482dcb24c719550a1f0aa7e7580dfc",
"packages": [
{
"name": "adaures/ipcat-php",
@ -286,6 +286,70 @@
},
"time": "2021-11-22T17:30:18+00:00"
},
{
"name": "codeigniter4/shield",
"version": "dev-develop",
"source": {
"type": "git",
"url": "https://github.com/codeigniter4/shield.git",
"reference": "f4cdfb672b600a032a6f0bfc0b7735411bee0cae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/codeigniter4/shield/zipball/f4cdfb672b600a032a6f0bfc0b7735411bee0cae",
"reference": "f4cdfb672b600a032a6f0bfc0b7735411bee0cae",
"shasum": ""
},
"require": {
"codeigniter4/settings": "^2.0",
"php": "^7.4.3 || ^8.0"
},
"provide": {
"codeigniter4/authentication-implementation": "1.0"
},
"require-dev": {
"codeigniter4/devkit": "^1.0",
"codeigniter4/framework": "^4.2.3",
"mockery/mockery": "^1.0"
},
"default-branch": true,
"type": "library",
"autoload": {
"files": [
"src/Helpers/auth_helper.php",
"src/Helpers/email_helper.php"
],
"psr-4": {
"CodeIgniter\\Shield\\": "src"
},
"exclude-from-classmap": ["**/Database/Migrations/**"]
},
"notification-url": "https://packagist.org/downloads/",
"license": ["MIT"],
"authors": [
{
"name": "Lonnie Ezell",
"email": "lonnieje@gmail.com",
"role": "Developer"
}
],
"description": "Authentication and Authorization for CodeIgniter 4",
"homepage": "https://github.com/codeigniter4/shield",
"keywords": [
"Authentication",
"authorization",
"codeigniter",
"codeigniter4"
],
"support": {
"docs": "https://github.com/codeigniter4/shield/blob/develop/docs/index.md",
"forum": "https://github.com/codeigniter4/shield/discussions",
"issues": "https://github.com/codeigniter4/shield/issues",
"slack": "https://codeigniterchat.slack.com",
"source": "https://github.com/codeigniter4/shield"
},
"time": "2022-10-05T10:11:44+00:00"
},
{
"name": "composer/ca-bundle",
"version": "1.3.4",
@ -1367,73 +1431,6 @@
},
"time": "2021-05-10T16:28:01+00:00"
},
{
"name": "myth/auth",
"version": "dev-develop",
"source": {
"type": "git",
"url": "https://github.com/lonnieezell/myth-auth.git",
"reference": "cc94231f5284e9578967aba4796f018809669c84"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/cc94231f5284e9578967aba4796f018809669c84",
"reference": "cc94231f5284e9578967aba4796f018809669c84",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"provide": {
"codeigniter4/authentication-implementation": "1.0"
},
"require-dev": {
"codeigniter4/codeigniter4-standard": "^1.0",
"codeigniter4/devkit": "^1.0",
"codeigniter4/framework": "^4.1",
"mockery/mockery": "^1.0"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Myth\\Auth\\": "src"
},
"exclude-from-classmap": ["**/Database/Migrations/**"]
},
"notification-url": "https://packagist.org/downloads/",
"license": ["MIT"],
"authors": [
{
"name": "Lonnie Ezell",
"email": "lonnieje@gmail.com",
"homepage": "http://newmythmedia.com",
"role": "Developer"
}
],
"description": "Flexible authentication/authorization system for CodeIgniter 4.",
"homepage": "https://github.com/lonnieezell/myth-auth",
"keywords": ["Authentication", "authorization", "codeigniter"],
"support": {
"issues": "https://github.com/lonnieezell/myth-auth/issues",
"source": "https://github.com/lonnieezell/myth-auth/tree/develop"
},
"funding": [
{
"url": "https://github.com/lonnieezell",
"type": "github"
},
{
"url": "https://github.com/mgatner",
"type": "github"
},
{
"url": "https://www.patreon.com/lonnieezell",
"type": "patreon"
}
],
"time": "2022-08-01T17:23:52+00:00"
},
{
"name": "nette/schema",
"version": "v1.2.2",
@ -6684,8 +6681,8 @@
"minimum-stability": "stable",
"stability-flags": {
"james-heinrich/getid3": 20,
"myth/auth": 20,
"michalsn/codeigniter4-uuid": 20
"michalsn/codeigniter4-uuid": 20,
"codeigniter4/shield": 20
},
"prefer-stable": true,
"prefer-lowest": false,

View File

@ -22,7 +22,6 @@ build:
script:
- npm run build
except:
- develop
- main
- beta
- alpha
@ -40,7 +39,6 @@ build-production:
- docs/.vitepress/dist
expire_in: 30 mins
only:
- develop
- main
- beta
- alpha
@ -72,7 +70,6 @@ deploy:
- rsync -avzuh -e "ssh -p $SSH_PORT" $SOURCE_FOLDER $USER@$HOST:$TEMP_DIRECTORY --progress
- ssh $USER@$HOST -p $SSH_PORT "rsync -rtv $TEMP_DIRECTORY $DIRECTORY"
only:
- develop
- main
- beta
- alpha

View File

@ -179,6 +179,7 @@ function getGuideSidebarEn() {
},
{ text: "Security", link: "/getting-started/security" },
{ text: "Update", link: "/getting-started/update" },
{ text: "Auth", link: "/getting-started/auth" },
],
},
{
@ -207,6 +208,7 @@ function getGuideSidebarFr() {
},
{ text: "Sécurité", link: "/fr/getting-started/security" },
{ text: "Mise à jour", link: "/fr/getting-started/update" },
{ text: "Authentification", link: "/fr/getting-started/auth" },
],
},
{
@ -235,6 +237,7 @@ function getGuideSidebarPtBR() {
},
{ text: "Segurança", link: "/pt-BR/getting-started/security" },
{ text: "Atualizar", link: "/pt-BR/getting-started/update" },
{ text: "Autenticação", link: "/pt-BR/getting-started/auth" },
],
},
{
@ -263,6 +266,7 @@ function getGuideSidebarNnNO() {
},
{ text: "Sikkerhet", link: "/nn-NO/getting-started/security" },
{ text: "Oppdaterer", link: "/nn-NO/getting-started/update" },
{ text: "Autentisering", link: "/pt-BR/getting-started/auth" },
],
},
{

View File

@ -0,0 +1,86 @@
---
title: Authentication & Authorization
sidebarDepth: 3
---
# Authentication & Authorization
Castopod handles authentication and authorization using `codeigniter/shield`
coupled with custom rules. Roles and permissions are defined at two levels:
1. [instance wide](#1-instance-wide-roles-and-permissions)
2. [per podcast](#2-per-podcast-roles-and-permissions)
## 1. Instance wide roles and permissions
### Instance roles
<!-- AUTH-INSTANCE-ROLES-LIST:START - Do not remove or modify this section -->
| role | description | permissions |
| ----------- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| Super admin | Has complete control over Castopod. | admin.\*, podcasts.\*, users.manage, persons.manage, pages.manage, fediverse.manage-blocks |
| Manager | Manages Castopod's content. | podcasts.create, podcasts.import, persons.manage, pages.manage |
| Podcaster | General users of Castopod. | admin.access |
<!-- AUTH-INSTANCE-ROLES-LIST:END -->
### Instance permissions
<!-- AUTH-INSTANCE-PERMISSIONS-LIST:START - Do not remove or modify this section -->
| permission | description |
| ----------------------- | ------------------------------------------------------------------ |
| admin.access | Can access the Castopod admin area. |
| admin.settings | Can access the Castopod settings. |
| users.manage | Can manage Castopod users. |
| persons.manage | Can manage persons. |
| pages.manage | Can manage pages. |
| podcasts.view | Can view all podcasts. |
| podcasts.create | Can create new podcasts. |
| podcasts.import | Can import podcasts. |
| fediverse.manage-blocks | Can block fediverse actors/domains from interacting with Castopod. |
<!-- AUTH-INSTANCE-PERMISSIONS-LIST:END -->
## 2. Per podcast roles and permissions
### Per podcast roles
<!-- AUTH-PODCAST-ROLES-LIST:START - Do not remove or modify this section -->
| role | description | permissions |
| ------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Admin | Has complete control of podcast #{id}. | \* |
| Editor | Manages content and publications of podcast #{id}. | view, edit, manage-import, manage-persons, manage-platforms, manage-publications, interact-as, episodes.view, episodes.create, episodes.edit, episodes.delete, episodes.manage-persons, episodes.manage-clips, episodes.manage-publications, episodes.manage-comments |
| Author | Manages content of podcast #{id} but cannot publish them. | view, manage-persons, episodes.view, episodes.create, episodes.edit, episodes.manage-persons, episodes.manage-clips |
| Guest | General contributor of the podcast #{id}. | view, episodes.view |
<!-- AUTH-PODCAST-ROLES-LIST:END -->
### Per podcast permissions
<!-- AUTH-PODCAST-PERMISSIONS-LIST:START - Do not remove or modify this section -->
| permission | description |
| ---------------------------- | ------------------------------------------------------------------------ |
| view | Can view dashboard and analytics of podcast #{id}. |
| edit | Can edit podcast #{id}. |
| delete | Can delete podcast #{id}. |
| manage-import | Can synchronize imported podcast #{id}. |
| manage-persons | Can manage subscriptions of podcast #{id}. |
| manage-subscriptions | Can manage subscriptions of podcast #{id}. |
| manage-contributors | Can manage contributors of podcast #{id}. |
| manage-platforms | Can set/remove platform links of podcast #{id}. |
| manage-publications | Can publish podcast #{id}. |
| interact-as | Can interact as the podcast #{id} to favourite, share or reply to posts. |
| episodes.view | Can view dashboard and analytics of podcast #{id}. |
| episodes.create | Can create episodes for podcast #{id}. |
| episodes.edit | Can edit podcast #{id}. |
| episodes.delete | Can delete podcast #{id}. |
| episodes.manage-persons | Can manage subscriptions of podcast #{id}. |
| episodes.manage-clips | Can manage video clips or soundbites of podcast #{id}. |
| episodes.manage-publications | Can publish podcast #{id}. |
| episodes.manage-comments | Can create/remove episode comments of podcast #{id}. |
<!-- AUTH-PODCAST-PERMISSIONS-LIST:END -->

View File

@ -25,60 +25,60 @@ $routes->group(
$routes->group('settings', static function ($routes): void {
$routes->get('/', 'SettingsController', [
'as' => 'settings-general',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->post('instance', 'SettingsController::attemptInstanceEdit', [
'as' => 'settings-instance',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->get('instance-delete-icon', 'SettingsController::deleteIcon', [
'as' => 'settings-instance-delete-icon',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->post('instance-images-regenerate', 'SettingsController::regenerateImages', [
'as' => 'settings-images-regenerate',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->post('instance-housekeeping-run', 'SettingsController::runHousekeeping', [
'as' => 'settings-housekeeping-run',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->get('theme', 'SettingsController::theme', [
'as' => 'settings-theme',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
$routes->post('theme', 'SettingsController::attemptSetInstanceTheme', [
'as' => 'settings-theme',
'filter' => 'permission:settings-manage',
'filter' => 'permission:admin.settings',
]);
});
$routes->group('persons', static function ($routes): void {
$routes->get('/', 'PersonController', [
'as' => 'person-list',
'filter' => 'permission:person-list',
'filter' => 'permission:persons.manage',
]);
$routes->get('new', 'PersonController::create', [
'as' => 'person-create',
'filter' => 'permission:person-create',
'filter' => 'permission:persons.manage',
]);
$routes->post('new', 'PersonController::attemptCreate', [
'filter' => 'permission:person-create',
'filter' => 'permission:persons.manage',
]);
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'PersonController::view/$1', [
'as' => 'person-view',
'filter' => 'permission:person-view',
'filter' => 'permission:persons.manage',
]);
$routes->get('edit', 'PersonController::edit/$1', [
'as' => 'person-edit',
'filter' => 'permission:person-edit',
'filter' => 'permission:persons.manage',
]);
$routes->post('edit', 'PersonController::attemptEdit/$1', [
'filter' => 'permission:person-edit',
'filter' => 'permission:persons.manage',
]);
$routes->add('delete', 'PersonController::delete/$1', [
'as' => 'person-delete',
'filter' => 'permission:person-delete',
'filter' => 'permission:persons.manage',
]);
});
});
@ -89,31 +89,31 @@ $routes->group(
]);
$routes->get('new', 'PodcastController::create', [
'as' => 'podcast-create',
'filter' => 'permission:podcasts-create',
'filter' => 'permission:podcasts.create',
]);
$routes->post('new', 'PodcastController::attemptCreate', [
'filter' => 'permission:podcasts-create',
'filter' => 'permission:podcasts.create',
]);
$routes->get('import', 'PodcastImportController', [
'as' => 'podcast-import',
'filter' => 'permission:podcasts-import',
'filter' => 'permission:podcasts.import',
]);
$routes->post('import', 'PodcastImportController::attemptImport', [
'filter' => 'permission:podcasts-import',
'filter' => 'permission:podcasts.import',
]);
// Podcast
// Use ids in admin area to help permission and group lookups
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'PodcastController::view/$1', [
'as' => 'podcast-view',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
]);
$routes->get('edit', 'PodcastController::edit/$1', [
'as' => 'podcast-edit',
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.edit',
]);
$routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.edit',
]);
$routes->get(
'publish',
@ -121,7 +121,7 @@ $routes->group(
[
'as' => 'podcast-publish',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.manage-publications',
],
);
$routes->post(
@ -129,7 +129,7 @@ $routes->group(
'PodcastController::attemptPublish/$1',
[
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.manage-publications',
],
);
$routes->get(
@ -138,7 +138,7 @@ $routes->group(
[
'as' => 'podcast-publish_edit',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.manage-publications',
],
);
$routes->post(
@ -146,7 +146,7 @@ $routes->group(
'PodcastController::attemptPublishEdit/$1',
[
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.manage-publications',
],
);
$routes->get(
@ -155,34 +155,34 @@ $routes->group(
[
'as' => 'podcast-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.manage-publications',
],
);
$routes->get('edit/delete-banner', 'PodcastController::deleteBanner/$1', [
'as' => 'podcast-banner-delete',
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.edit',
]);
$routes->get('delete', 'PodcastController::delete/$1', [
'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete',
'filter' => 'permission:podcast#.delete',
]);
$routes->post('delete', 'PodcastController::attemptDelete/$1', [
'filter' => 'permission:podcasts-delete',
'filter' => 'permission:podcast#.delete',
]);
$routes->get('update', 'PodcastImportController::updateImport/$1', [
'as' => 'podcast-update-feed',
'filter' => 'permission:podcasts-import',
'filter' => 'permission:podcast#.manage-import',
]);
$routes->group('persons', static function ($routes): void {
$routes->get('/', 'PodcastPersonController/$1', [
'as' => 'podcast-persons-manage',
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.manage-persons',
]);
$routes->post(
'/',
'PodcastPersonController::attemptAdd/$1',
[
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.manage-persons',
],
);
$routes->get(
@ -190,21 +190,21 @@ $routes->group(
'PodcastPersonController::remove/$1/$2',
[
'as' => 'podcast-person-remove',
'filter' => 'permission:podcast-edit',
'filter' => 'permission:podcast#.manage-persons',
],
);
});
$routes->group('analytics', static function ($routes): void {
$routes->get('/', 'PodcastController::viewAnalytics/$1', [
'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
]);
$routes->get(
'webpages',
'PodcastController::viewAnalyticsWebpages/$1',
[
'as' => 'podcast-analytics-webpages',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
$routes->get(
@ -212,7 +212,7 @@ $routes->group(
'PodcastController::viewAnalyticsLocations/$1',
[
'as' => 'podcast-analytics-locations',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
$routes->get(
@ -220,7 +220,7 @@ $routes->group(
'PodcastController::viewAnalyticsUniqueListeners/$1',
[
'as' => 'podcast-analytics-unique-listeners',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
$routes->get(
@ -228,7 +228,7 @@ $routes->group(
'PodcastController::viewAnalyticsListeningTime/$1',
[
'as' => 'podcast-analytics-listening-time',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
$routes->get(
@ -236,7 +236,7 @@ $routes->group(
'PodcastController::viewAnalyticsTimePeriods/$1',
[
'as' => 'podcast-analytics-time-periods',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
$routes->get(
@ -244,7 +244,7 @@ $routes->group(
'PodcastController::viewAnalyticsPlayers/$1',
[
'as' => 'podcast-analytics-players',
'filter' => 'permission:podcasts-view,podcast-view',
'filter' => 'permission:podcast#.view',
],
);
});
@ -253,17 +253,17 @@ $routes->group(
$routes->get('/', 'EpisodeController::list/$1', [
'as' => 'episode-list',
'filter' =>
'permission:episodes-list,podcast_episodes-list',
'permission:podcast#.episodes.view',
]);
$routes->get('new', 'EpisodeController::create/$1', [
'as' => 'episode-create',
'filter' => 'permission:podcast_episodes-create',
'filter' => 'permission:podcast#.episodes.create',
]);
$routes->post(
'new',
'EpisodeController::attemptCreate/$1',
[
'filter' => 'permission:podcast_episodes-create',
'filter' => 'permission:podcast#.episodes.create',
],
);
// Episode
@ -271,17 +271,17 @@ $routes->group(
$routes->get('/', 'EpisodeController::view/$1/$2', [
'as' => 'episode-view',
'filter' =>
'permission:episodes-view,podcast_episodes-view',
'permission:podcast#.episodes.view',
]);
$routes->get('edit', 'EpisodeController::edit/$1/$2', [
'as' => 'episode-edit',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.edit',
]);
$routes->post(
'edit',
'EpisodeController::attemptEdit/$1/$2',
[
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.edit',
],
);
$routes->get(
@ -290,7 +290,7 @@ $routes->group(
[
'as' => 'episode-publish',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->post(
@ -298,7 +298,7 @@ $routes->group(
'EpisodeController::attemptPublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->get(
@ -307,7 +307,7 @@ $routes->group(
[
'as' => 'episode-publish_edit',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->post(
@ -315,7 +315,7 @@ $routes->group(
'EpisodeController::attemptPublishEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->get(
@ -324,7 +324,7 @@ $routes->group(
[
'as' => 'episode-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->get(
@ -350,7 +350,7 @@ $routes->group(
[
'as' => 'episode-unpublish',
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->post(
@ -358,7 +358,7 @@ $routes->group(
'EpisodeController::attemptUnpublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
'permission:podcast#.episodes.manage-publications',
],
);
$routes->get(
@ -367,7 +367,7 @@ $routes->group(
[
'as' => 'episode-delete',
'filter' =>
'permission:podcast_episodes-delete',
'permission:podcast#.episodes.delete',
],
);
$routes->post(
@ -375,7 +375,7 @@ $routes->group(
'EpisodeController::attemptDelete/$1/$2',
[
'filter' =>
'permission:podcast_episodes-delete',
'permission:podcast#.episodes.delete',
],
);
$routes->get(
@ -383,7 +383,7 @@ $routes->group(
'EpisodeController::transcriptDelete/$1/$2',
[
'as' => 'transcript-delete',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.edit',
],
);
$routes->get(
@ -391,7 +391,7 @@ $routes->group(
'EpisodeController::chaptersDelete/$1/$2',
[
'as' => 'chapters-delete',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.edit',
],
);
$routes->get(
@ -399,7 +399,7 @@ $routes->group(
'SoundbiteController::list/$1/$2',
[
'as' => 'soundbites-list',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -407,7 +407,7 @@ $routes->group(
'SoundbiteController::create/$1/$2',
[
'as' => 'soundbites-create',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->post(
@ -415,7 +415,7 @@ $routes->group(
'SoundbiteController::attemptCreate/$1/$2',
[
'as' => 'soundbites-create',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -423,7 +423,7 @@ $routes->group(
'SoundbiteController::delete/$1/$2/$3',
[
'as' => 'soundbites-delete',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -431,7 +431,7 @@ $routes->group(
'VideoClipsController::list/$1/$2',
[
'as' => 'video-clips-list',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -439,7 +439,7 @@ $routes->group(
'VideoClipsController::create/$1/$2',
[
'as' => 'video-clips-create',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->post(
@ -447,7 +447,7 @@ $routes->group(
'VideoClipsController::attemptCreate/$1/$2',
[
'as' => 'video-clips-create',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -455,7 +455,7 @@ $routes->group(
'VideoClipsController::view/$1/$2/$3',
[
'as' => 'video-clip',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -463,7 +463,7 @@ $routes->group(
'VideoClipsController::retry/$1/$2/$3',
[
'as' => 'video-clip-retry',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -471,7 +471,7 @@ $routes->group(
'VideoClipsController::delete/$1/$2/$3',
[
'as' => 'video-clip-delete',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-clips',
],
);
$routes->get(
@ -479,20 +479,20 @@ $routes->group(
'EpisodeController::embed/$1/$2',
[
'as' => 'embed-add',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.edit',
],
);
$routes->group('persons', static function ($routes): void {
$routes->get('/', 'EpisodePersonController/$1/$2', [
'as' => 'episode-persons-manage',
'filter' => 'permission:podcast_episodes-edit',
'filter' => 'permission:podcast#.episodes.manage-persons',
]);
$routes->post(
'/',
'EpisodePersonController::attemptAdd/$1/$2',
[
'filter' =>
'permission:podcast_episodes-edit',
'permission:podcast#.episodes.manage-persons',
],
);
$routes->get(
@ -501,7 +501,7 @@ $routes->group(
[
'as' => 'episode-person-remove',
'filter' =>
'permission:podcast_episodes-edit',
'permission:podcast#.episodes.manage-persons',
],
);
});
@ -511,7 +511,7 @@ $routes->group(
'EpisodeController::attemptCommentCreate/$1/$2',
[
'as' => 'comment-attempt-create',
'filter' => 'permission:podcast-manage_publications',
'filter' => 'permission:podcast#.episodes.manage-comments',
]
);
$routes->post(
@ -519,7 +519,7 @@ $routes->group(
'EpisodeController::attemptCommentReply/$1/$2/$3',
[
'as' => 'comment-attempt-reply',
'filter' => 'permission:podcast-manage_publications',
'filter' => 'permission:podcast#.episodes.manage-comments',
]
);
$routes->post(
@ -527,73 +527,19 @@ $routes->group(
'EpisodeController::attemptCommentDelete/$1/$2',
[
'as' => 'comment-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
'filter' => 'permission:podcast#.episodes.manage-comments',
]
);
});
});
});
// Podcast contributors
$routes->group('contributors', static function ($routes): void {
$routes->get('/', 'ContributorController::list/$1', [
'as' => 'contributor-list',
'filter' =>
'permission:podcasts-view,podcast-manage_contributors',
]);
$routes->get('add', 'ContributorController::add/$1', [
'as' => 'contributor-add',
'filter' => 'permission:podcast-manage_contributors',
]);
$routes->post(
'add',
'ContributorController::attemptAdd/$1',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
// Contributor
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'ContributorController::view/$1/$2', [
'as' => 'contributor-view',
'filter' =>
'permission:podcast-manage_contributors',
]);
$routes->get(
'edit',
'ContributorController::edit/$1/$2',
[
'as' => 'contributor-edit',
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->post(
'edit',
'ContributorController::attemptEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->get(
'remove',
'ContributorController::remove/$1/$2',
[
'as' => 'contributor-remove',
'filter' =>
'permission:podcast-manage_contributors',
],
);
});
});
$routes->group('platforms', static function ($routes): void {
$routes->get(
'/',
'PodcastPlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast-manage_platforms',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
@ -601,7 +547,7 @@ $routes->group(
'PodcastPlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast-manage_platforms',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
@ -609,7 +555,7 @@ $routes->group(
'PodcastPlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast-manage_platforms',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->post(
@ -617,7 +563,7 @@ $routes->group(
'PodcastPlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast-manage_platforms',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
@ -625,7 +571,7 @@ $routes->group(
'PodcastPlatformController::removePodcastPlatform/$1/$2',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast-manage_platforms',
'filter' => 'permission:podcast#.manage-platforms',
],
);
});
@ -633,12 +579,15 @@ $routes->group(
$routes->group('notifications', static function ($routes): void {
$routes->get('/', 'NotificationController::list/$1', [
'as' => 'notification-list',
'filter' => 'permission:podcast#.view',
]);
$routes->get('(:num)/mark-as-read', 'NotificationController::markAsRead/$1/$2', [
'as' => 'notification-mark-as-read',
'filter' => 'permission:podcast#.manage-notifications',
]);
$routes->get('mark-all-as-read', 'NotificationController::markAllAsRead/$1', [
'as' => 'notification-mark-all-as-read',
'filter' => 'permission:podcast#.manage-notifications',
]);
});
});
@ -653,7 +602,7 @@ $routes->group(
'FediverseController::blockedActors',
[
'as' => 'fediverse-blocked-actors',
'filter' => 'permission:fediverse-block_actors',
'filter' => 'permission:fediverse.manage-blocks',
],
);
$routes->get(
@ -661,7 +610,7 @@ $routes->group(
'FediverseController::blockedDomains',
[
'as' => 'fediverse-blocked-domains',
'filter' => 'permission:fediverse-block_domains',
'filter' => 'permission:fediverse.manage-blocks',
],
);
});
@ -669,13 +618,14 @@ $routes->group(
$routes->group('pages', static function ($routes): void {
$routes->get('/', 'PageController::list', [
'as' => 'page-list',
'filter' => 'permission:pages.manage',
]);
$routes->get('new', 'PageController::create', [
'as' => 'page-create',
'filter' => 'permission:pages-manage',
'filter' => 'permission:pages.manage',
]);
$routes->post('new', 'PageController::attemptCreate', [
'filter' => 'permission:pages-manage',
'filter' => 'permission:pages.manage',
]);
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'PageController::view/$1', [
@ -683,78 +633,16 @@ $routes->group(
]);
$routes->get('edit', 'PageController::edit/$1', [
'as' => 'page-edit',
'filter' => 'permission:pages-manage',
'filter' => 'permission:pages.manage',
]);
$routes->post('edit', 'PageController::attemptEdit/$1', [
'filter' => 'permission:pages-manage',
'filter' => 'permission:pages.manage',
]);
$routes->get('delete', 'PageController::delete/$1', [
'as' => 'page-delete',
'filter' => 'permission:pages-manage',
'filter' => 'permission:pages.manage',
]);
});
});
// Users
$routes->group('users', static function ($routes): void {
$routes->get('/', 'UserController::list', [
'as' => 'user-list',
'filter' => 'permission:users-list',
]);
$routes->get('new', 'UserController::create', [
'as' => 'user-create',
'filter' => 'permission:users-create',
]);
$routes->post('new', 'UserController::attemptCreate', [
'filter' => 'permission:users-create',
]);
// User
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'UserController::view/$1', [
'as' => 'user-view',
'filter' => 'permission:users-view',
]);
$routes->get('edit', 'UserController::edit/$1', [
'as' => 'user-edit',
'filter' => 'permission:users-manage_authorizations',
]);
$routes->post('edit', 'UserController::attemptEdit/$1', [
'filter' => 'permission:users-manage_authorizations',
]);
$routes->get('ban', 'UserController::ban/$1', [
'as' => 'user-ban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get('unban', 'UserController::unBan/$1', [
'as' => 'user-unban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get(
'force-pass-reset',
'UserController::forcePassReset/$1',
[
'as' => 'user-force_pass_reset',
'filter' => 'permission:users-force_pass_reset',
],
);
$routes->get('delete', 'UserController::delete/$1', [
'as' => 'user-delete',
'filter' => 'permission:users-delete',
]);
});
});
// My account
$routes->group('my-account', static function ($routes): void {
$routes->get('/', 'MyAccountController', [
'as' => 'my-account',
]);
$routes->get(
'change-password',
'MyAccountController::changePassword/$1',
[
'as' => 'change-password',
],
);
$routes->post('change-password', 'MyAccountController::attemptChange/$1');
});
},
);

View File

@ -1,203 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Admin\Controllers;
use App\Entities\Podcast;
use App\Models\PodcastModel;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Exception;
use Modules\Auth\Authorization\GroupModel;
use Modules\Auth\Entities\User;
class ContributorController extends BaseController
{
protected Podcast $podcast;
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
if (($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) <= 1) {
return $this->{$method}();
}
if (($this->user = (new UserModel())->getPodcastContributor((int) $params[1], (int) $params[0])) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('contributor/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
'contributor' => (new UserModel())->getPodcastContributor($this->user->id, $this->podcast->id),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('contributor/view', $data);
}
public function add(): string
{
helper('form');
$users = (new UserModel())->findAll();
$userOptions = array_reduce(
$users,
static function ($result, $user) {
$result[$user->id] = $user->username;
return $result;
},
[],
);
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
static function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'userOptions' => $userOptions,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('contributor/add', $data);
}
public function attemptAdd(): RedirectResponse
{
try {
(new PodcastModel())->addPodcastContributor(
(int) $this->request->getPost('user'),
$this->podcast->id,
(int) $this->request->getPost('role'),
);
} catch (Exception) {
return redirect()
->back()
->withInput()
->with('errors', [lang('Contributor.messages.alreadyAddedError')]);
}
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
static function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'user' => $this->user,
'contributorGroupId' => (new PodcastModel())->getContributorGroupId(
$this->user->id,
$this->podcast->id,
),
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('contributor/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
(new PodcastModel())->updatePodcastContributor(
$this->user->id,
$this->podcast->id,
(int) $this->request->getPost('role'),
);
return redirect()->route('contributor-edit', [$this->podcast->id, $this->user->id])->with(
'message',
lang('Contributor.messages.editSuccess')
);
}
public function remove(): RedirectResponse
{
if ($this->podcast->created_by === $this->user->id) {
return redirect()
->back()
->with('errors', [lang('Contributor.messages.removeOwnerError')]);
}
$podcastModel = new PodcastModel();
if (
! $podcastModel->removePodcastContributor($this->user->id, $this->podcast->id)
) {
return redirect()
->back()
->with('errors', $podcastModel->errors());
}
return redirect()
->route('contributor-list', [$this->podcast->id])
->with(
'message',
lang('Contributor.messages.removeSuccess', [
'username' => $this->user->username,
'podcastTitle' => $this->podcast->title,
]),
);
}
}

View File

@ -95,7 +95,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('episode/list', $data);
}
@ -108,7 +108,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/view', $data);
@ -125,7 +125,7 @@ class EpisodeController extends BaseController
'nextEpisodeNumber' => (new EpisodeModel())->getNextEpisodeNumber($this->podcast->id, $currentSeasonNumber),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('episode/create', $data);
}
@ -261,7 +261,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/edit', $data);
@ -438,7 +438,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/publish', $data);
@ -551,7 +551,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/publish_edit', $data);
@ -851,7 +851,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/delete', $data);
@ -949,7 +949,7 @@ class EpisodeController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/embed', $data);

View File

@ -59,7 +59,7 @@ class EpisodePersonController extends BaseController
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/persons', $data);

View File

@ -67,7 +67,7 @@ class NotificationController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/notifications', $data);

View File

@ -23,7 +23,6 @@ use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use Config\Services;
use Modules\Analytics\Models\AnalyticsPodcastByCountryModel;
use Modules\Analytics\Models\AnalyticsPodcastByEpisodeModel;
use Modules\Analytics\Models\AnalyticsPodcastByHourModel;
@ -56,13 +55,13 @@ class PodcastController extends BaseController
public function list(): string
{
if (! has_permission('podcasts-list')) {
if (auth()->user()->can('podcasts.view')) {
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts((int) user_id()),
'podcasts' => (new PodcastModel())->findAll(),
];
} else {
$data = [
'podcasts' => (new PodcastModel())->findAll(),
'podcasts' => get_user_podcasts(auth()->user()),
];
}
@ -76,7 +75,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/view', $data);
}
@ -88,7 +87,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/index', $data);
}
@ -100,7 +99,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/webpages', $data);
}
@ -112,7 +111,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/locations', $data);
}
@ -124,7 +123,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/unique_listeners', $data);
}
@ -136,7 +135,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/listening_time', $data);
}
@ -148,7 +147,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/time_periods', $data);
}
@ -160,7 +159,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/analytics/players', $data);
}
@ -253,10 +252,11 @@ class PodcastController extends BaseController
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
// generate podcast roles and permissions
// before setting current user as podcast admin
config('AuthGroups')
->generatePodcastAuthorizations($newPodcastId);
add_podcast_group(auth()->user(), (int) $newPodcastId, setting('AuthGroups.mostPowerfulPodcastGroup'));
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
@ -264,10 +264,6 @@ class PodcastController extends BaseController
$this->request->getPost('other_categories') ?? [],
);
// set interact as the newly created podcast actor
$createdPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($createdPodcast->actor_id);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId])->with(
@ -290,7 +286,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/edit', $data);
}
@ -444,7 +440,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/delete', $data);
}
@ -576,15 +572,6 @@ class PodcastController extends BaseController
}
}
if ($this->podcast->actor_id === interact_as_actor_id()) {
//set interact to the most recently created podcast actor
$mostRecentPodcast = (new PodcastModel())->orderBy('created_at', 'desc')
->first();
if ($mostRecentPodcast !== null) {
set_interact_as_actor($mostRecentPodcast->actor_id);
}
}
$db->transComplete();
//delete podcast media files and folder
@ -620,7 +607,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/publish', $data);
@ -754,7 +741,7 @@ class PodcastController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/publish_edit', $data);

View File

@ -23,7 +23,6 @@ use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use ErrorException;
use League\HTMLToMarkdown\HtmlConverter;
@ -201,10 +200,11 @@ class PodcastImportController extends BaseController
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
// set current user as podcast admin
// 1. create new group
config('AuthGroups')
->generatePodcastAuthorizations($newPodcastId);
add_podcast_group(auth()->user(), $newPodcastId, 'admin');
$podcastsPlatformsData = [];
$platformTypes = [
@ -460,9 +460,7 @@ class PodcastImportController extends BaseController
}
}
// set interact as the newly imported podcast actor
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($importedPodcast->actor_id);
// set podcast publication date
$importedPodcast->published_at = $firstEpisodePublicationDate ?? $importedPodcast->created_at;

View File

@ -47,7 +47,7 @@ class PodcastPersonController extends BaseController
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/persons', $data);
}

View File

@ -53,7 +53,7 @@ class PodcastPlatformController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
]);
return view('podcast/platforms', $data);

View File

@ -77,7 +77,7 @@ class SoundbiteController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/soundbites_list', $data);
@ -93,7 +93,7 @@ class SoundbiteController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/soundbites_new', $data);

View File

@ -1,258 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Admin\Controllers;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use Modules\Auth\Authorization\GroupModel;
use Modules\Auth\Entities\User;
class UserController extends BaseController
{
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
return $this->{$method}();
}
if ($this->user = (new UserModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'users' => (new UserModel())->findAll(),
];
return view('user/list', $data);
}
public function view(): string
{
$data = [
'user' => $this->user,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('user/view', $data);
}
public function create(): string
{
helper('form');
$data = [
'roles' => (new GroupModel())->getUserRoles(),
];
return view('user/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules([
'only' => ['username'],
]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
],
);
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new User($this->request->getPost());
// Activate user
$user->activate();
// Force user to reset his password on first connection
$user->forcePasswordReset();
if (! $userModel->insert($user)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.createSuccess', [
'username' => $user->username,
]));
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getUserRoles();
$roleOptions = array_reduce(
$roles,
static function ($result, $role) {
$result[$role->name] = lang('User.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'user' => $this->user,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('user/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$authorize = Services::authorization();
$roles = $this->request->getPost('roles');
if ($this->user->isOwner) {
return redirect()
->back()
->with('errors', [
lang('User.messages.editOwnerError', [
'username' => $this->user->username,
]),
]);
}
$authorize->setUserGroups($this->user->id, $roles ?? []);
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.rolesEditSuccess', [
'username' => $this->user->username,
]));
}
public function forcePassReset(): RedirectResponse
{
$userModel = new UserModel();
$this->user->forcePasswordReset();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.forcePassResetSuccess', [
'username' => $this->user->username,
]),
);
}
public function ban(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.banSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
$userModel = new UserModel();
// TODO: add ban reason?
$this->user->ban('');
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.banSuccess', [
'username' => $this->user->username,
]));
}
public function unBan(): RedirectResponse
{
$userModel = new UserModel();
$this->user->unBan();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.unbanSuccess', [
'username' => $this->user->username,
]));
}
public function delete(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
(new UserModel())->delete($this->user->id);
return redirect()
->back()
->with('message', lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
]));
}
}

View File

@ -82,7 +82,7 @@ class VideoClipsController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);
return view('episode/video_clips_list', $data);
@ -99,7 +99,7 @@ class VideoClipsController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
2 => $videoClip->title,
]);
@ -114,7 +114,7 @@ class VideoClipsController extends BaseController
];
replace_breadcrumb_params([
0 => $this->podcast->title,
0 => $this->podcast->at_handle,
1 => $this->episode->title,
]);

View File

@ -28,6 +28,7 @@ return [
'publish-date-edit' => 'edit publication date',
'unpublish' => 'unpublish',
'delete' => 'delete',
'remove' => 'remove',
'fediverse' => 'fediverse',
'block-lists' => 'block lists',
'users' => 'users',

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'edit_roles' => "Edit {username}'s roles",
'forcePassReset' => 'Force pass reset',
'ban' => 'Ban',
'unban' => 'Unban',
'delete' => 'Delete',
'create' => 'New user',
'view' => "{username}'s info",
'all_users' => 'All users',
'list' => [
'user' => 'User',
'roles' => 'Roles',
'banned' => 'Banned?',
],
'form' => [
'email' => 'Email',
'username' => 'Username',
'password' => 'Password',
'new_password' => 'New Password',
'roles' => 'Roles',
'permissions' => 'Permissions',
'submit_create' => 'Create user',
'submit_edit' => 'Save',
'submit_password_change' => 'Change!',
],
'roles' => [
'superadmin' => 'Super admin',
],
'messages' => [
'createSuccess' =>
'User created successfully! {username} will be prompted with a password reset upon first authentication.',
'rolesEditSuccess' =>
"{username}'s roles have been successfully updated.",
'forcePassResetSuccess' =>
'{username} will be prompted with a password reset upon next visit.',
'banSuccess' => '{username} has been banned.',
'unbanSuccess' => '{username} has been unbanned.',
'editOwnerError' =>
'{username} is the instance owner, you cannot edit its roles.',
'banSuperAdminError' =>
'{username} is a superadmin, one does not simply ban a superadmin…',
'deleteSuperAdminError' =>
'{username} is a superadmin, one does not simply delete a superadmin…',
'deleteSuccess' => '{username} has been deleted.',
],
];

View File

@ -20,9 +20,9 @@ class Analytics extends BaseConfig
* @var array<string, string>
*/
public array $routeFilters = [
'analytics-full-data' => 'permission:podcasts-view,podcast-view',
'analytics-data' => 'permission:podcasts-view,podcast-view',
'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
'analytics-full-data' => 'permission:podcast#.view',
'analytics-data' => 'permission:podcast#.view',
'analytics-filtered-data' => 'permission:podcast#.view',
];
/**

42
modules/Auth/Auth.php Normal file
View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Modules\Auth;
use CodeIgniter\Router\RouteCollection;
use CodeIgniter\Shield\Auth as ShieldAuth;
class Auth extends ShieldAuth
{
/**
* Will set the routes in your application to use
* the Shield auth routes.
*
* Usage (in Config/Routes.php):
* - auth()->routes($routes);
* - auth()->routes($routes, ['except' => ['login', 'register']])
*/
public function routes(RouteCollection &$routes, array $config = []): void
{
$authRoutes = config('AuthRoutes')
->routes;
$routes->group(config('Auth')->gateway, [
'namespace' => 'Modules\Auth\Controllers',
], static function (RouteCollection $routes) use ($authRoutes, $config): void {
foreach ($authRoutes as $name => $row) {
if (! isset($config['except']) || ! in_array($name, $config['except'], true)) {
foreach ($row as $params) {
$options = isset($params[3])
? [
'as' => $params[3],
]
: null;
$routes->{$params[0]}($params[1], $params[2], $options);
}
}
}
});
}
}

View File

@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Authorization;
use Myth\Auth\Authorization\FlatAuthorization as MythAuthFlatAuthorization;
class FlatAuthorization extends MythAuthFlatAuthorization
{
/**
* The group model to use. Usually the class noted below (or an extension thereof) but can be any compatible
* CodeIgniter Model.
*
* @var PermissionModel
*/
protected $permissionModel;
/**
* Checks a group to see if they have the specified permission.
*/
public function groupHasPermission(int | string $permission, int $groupId): bool
{
// Get the Permission ID
$permissionId = $this->getPermissionID($permission);
if (! is_numeric($permissionId)) {
return false;
}
return $this->permissionModel->doesGroupHavePermission($groupId, $permissionId);
}
/**
* Makes user part of given groups.
*
* @param array<string, string> $groups Either collection of ID or names
*/
public function setUserGroups(int $userId, array $groups = []): bool
{
// remove user from all groups before resetting it in new groups
$this->groupModel->removeUserFromAllGroups($userId);
if ($groups === []) {
return true;
}
foreach ($groups as $group) {
$this->addUserToGroup($userId, $group);
}
return true;
}
}

View File

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Authorization;
use Myth\Auth\Authorization\GroupModel as MythAuthGroupModel;
class GroupModel extends MythAuthGroupModel
{
/**
* @return mixed[]
*/
public function getContributorRoles(): array
{
return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after')
->findAll();
}
/**
* @return mixed[]
*/
public function getUserRoles(): array
{
return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after')
->findAll();
}
}

View File

@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Authorization;
use Myth\Auth\Authorization\PermissionModel as MythAuthPermissionModel;
class PermissionModel extends MythAuthPermissionModel
{
/**
* Checks to see if a user, or one of their groups, has a specific permission.
*/
public function doesGroupHavePermission(int $groupId, int $permissionId): bool
{
// Check group permissions and take advantage of caching
$groupPerms = $this->getPermissionsForGroup($groupId);
return count($groupPerms) &&
array_key_exists($permissionId, $groupPerms);
}
/**
* Gets all permissions for a group in a way that can be easily used to check against:
*
* [ id => name, id => name ]
*
* @return array<int, string>
*/
public function getPermissionsForGroup(int $groupId): array
{
$cacheName = "group{$groupId}_permissions";
if (! ($found = cache($cacheName))) {
$groupPermissions = $this->db
->table('auth_groups_permissions')
->select('id, auth_permissions.name')
->join('auth_permissions', 'auth_permissions.id = permission_id', 'inner')
->where('group_id', $groupId)
->get()
->getResultObject();
$found = [];
foreach ($groupPermissions as $row) {
$found[$row->id] = strtolower($row->name);
}
cache()
->save($cacheName, $found, 300);
}
return $found;
}
}

View File

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Commands;
use Closure;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\View\Table;
use Config\Services;
use League\HTMLToMarkdown\Converter\TableConverter;
use League\HTMLToMarkdown\HtmlConverter;
use Modules\Auth\Config\AuthGroups;
class RolesDoc extends BaseCommand
{
/**
* @var array<string, string>
*/
private const COMMENT_BLOCK_IDS = [
'instance_roles' => 'AUTH-INSTANCE-ROLES-LIST',
'instance_permissions' => 'AUTH-INSTANCE-PERMISSIONS-LIST',
'podcast_roles' => 'AUTH-PODCAST-ROLES-LIST',
'podcast_permissions' => 'AUTH-PODCAST-PERMISSIONS-LIST',
];
/**
* @var string
*/
protected $group = 'auth';
/**
* @var string
*/
protected $name = 'auth:generate-doc';
/**
* @var string
*/
protected $description = 'Generates the html table references for roles and permissions in the docs.';
public function run(array $params): void
{
// loop over all files in path
$defaultFile = glob(ROOTPATH . 'docs/src/getting-started/auth.md');
$localizedFiles = glob(ROOTPATH . 'docs/src/**/getting-started/auth.md') ?? [];
$files = array_merge($defaultFile, $localizedFiles);
CLI::write(implode(', ', $files));
if ($files === []) {
return;
}
foreach ($files as $file) {
$locale = $this->detectLocaleFromPath($file);
$language = Services::language();
$language->setLocale($locale);
$authGroups = new AuthGroups();
$fileContents = file_get_contents($file);
foreach (self::COMMENT_BLOCK_IDS as $key => $block_id) {
$pattern = '/(<!--\s' . $block_id . ':START.*-->)[\S\s]*(<!--\s' . $block_id . ':END.*-->)/';
$handleInjectMethod = 'handle' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key)));
$fileContents = $this->{$handleInjectMethod}($authGroups, $fileContents, $pattern);
}
// Write the contents back to the file
file_put_contents($file, $fileContents);
}
}
protected function handleInstanceRoles($authGroups, string $fileContents, string $pattern): string
{
$instanceMatrix = $authGroups->matrix;
return $this->renderCommentBlock(
$fileContents,
$pattern,
['role', 'description', 'permissions'],
$authGroups->instanceGroups,
static function ($table, $key, $value) use ($instanceMatrix): void {
$table->addRow($value['title'], $value['description'], implode(', ', $instanceMatrix[$key]));
}
);
}
protected function handleInstancePermissions($authGroups, string $fileContents, string $pattern): string
{
return $this->renderCommentBlock(
$fileContents,
$pattern,
['permission', 'description'],
$authGroups->instancePermissions,
static function ($table, $key, $value): void {
$table->addRow($key, $value);
}
);
}
protected function handlePodcastRoles($authGroups, string $fileContents, string $pattern): string
{
$podcastMatrix = $authGroups->podcastMatrix;
return $this->renderCommentBlock(
$fileContents,
$pattern,
['role', 'description', 'permissions'],
$authGroups->podcastGroups,
static function ($table, $key, $value) use ($podcastMatrix): void {
$table->addRow($value['title'], $value['description'], implode(', ', $podcastMatrix[$key]));
}
);
}
protected function handlePodcastPermissions($authGroups, string $fileContents, string $pattern): string
{
return $this->renderCommentBlock(
$fileContents,
$pattern,
['permission', 'description'],
$authGroups->podcastPermissions,
static function ($table, $key, $value): void {
$table->addRow($key, $value);
}
);
}
private function renderCommentBlock(
string $fileContents,
string $pattern,
array $tableHeading,
array $data,
Closure $callback
): string {
// check if it has the start and end comments to insert roles table
// looking for <AUTH-INSTANCE-ROLES-LIST:START> and <AUTH-INSTANCE-ROLES-LIST:END>
$hasInstanceInsertComments = preg_match($pattern, $fileContents);
if (! $hasInstanceInsertComments) {
return $fileContents;
}
// prepare role table
$table = new Table();
$table->setHeading($tableHeading);
foreach ($data as $key => $value) {
$callback($table, $key, $value);
}
$converter = new HtmlConverter();
$converter->getEnvironment()
->addConverter(new TableConverter());
$markdownTable = $converter->convert($table->generate());
// insert table between block comments
$newFileContents = preg_replace(
$pattern,
'${1}' . PHP_EOL . PHP_EOL . $markdownTable . PHP_EOL . PHP_EOL . '${2}',
$fileContents
);
if ($newFileContents === null) {
return $fileContents;
}
return $newFileContents;
}
private function detectLocaleFromPath($filePath): string
{
preg_match('~docs\/src\/(?:([a-z]{2}(?:-[A-Za-z]{2,})?)\/)getting-started\/auth\.md~', $filePath, $match);
if ($match === []) {
return 'en';
}
return $match[1];
}
}

View File

@ -4,46 +4,99 @@ declare(strict_types=1);
namespace Modules\Auth\Config;
use Myth\Auth\Config\Auth as MythAuthConfig;
use CodeIgniter\Shield\Authentication\Actions\ActionInterface;
use CodeIgniter\Shield\Config\Auth as ShieldAuth;
use Modules\Auth\Models\UserModel;
class Auth extends MythAuthConfig
class Auth extends ShieldAuth
{
/**
* --------------------------------------------------------------------------
* Views used by Auth Controllers
* --------------------------------------------------------------------------
* ////////////////// AUTHENTICATION //////////////////
*
* @var array<string, string>
*/
public $views = [
public array $views = [
'login' => 'login',
'register' => 'register',
'forgot' => 'forgot',
'reset' => 'reset',
'emailForgot' => 'emails/forgot',
'emailActivation' => 'emails/activation',
'layout' => '_layout',
'action_email_2fa' => 'email_2fa_show',
'action_email_2fa_verify' => 'email_2fa_verify',
'action_email_2fa_email' => 'emails/email_2fa_email',
'action_email_activate_show' => 'email_activate_show',
'action_email_activate_email' => 'emails/email_activate_email',
'magic-link-login' => 'magic_link_form',
'magic-link-message' => 'magic_link_message',
'magic-link-email' => 'emails/magic_link_email',
'magic-link-set-password' => 'magic_link_set_password',
'welcome-email' => 'emails/welcome_email',
];
/**
* --------------------------------------------------------------------------
* Layout for the views to extend
* --------------------------------------------------------------------------
* --------------------------------------------------------------------
* Redirect urLs
* --------------------------------------------------------------------
* The default URL that a user will be redirected to after
* various auth actions. If you need more flexibility you can
* override the `getUrl()` method to apply any logic you may need.
*
* @var string
* @var array<string, string>
*/
public $viewLayout = '_layout';
public array $redirects = [
'register' => '/',
'login' => '/',
'logout' => 'login',
];
/**
* --------------------------------------------------------------------------
* Allow User Registration
* --------------------------------------------------------------------------
* When enabled (default) any unregistered user may apply for a new
* account. If you disable registration you may need to ensure your
* controllers and views know not to offer registration.
* --------------------------------------------------------------------
* Authentication Actions
* --------------------------------------------------------------------
* Specifies the class that represents an action to take after
* the user logs in or registers a new account at the site.
*
* @var bool
* You must register actions in the order of the actions to be performed.
*
* Available actions with Shield:
* - register: 'CodeIgniter\Shield\Authentication\Actions\EmailActivator'
* - login: 'CodeIgniter\Shield\Authentication\Actions\Email2FA'
*
* @var array<string, class-string<ActionInterface>|null>
*/
public $allowRegistration = false;
public array $actions = [
'register' => null,
'login' => null,
];
/**
* --------------------------------------------------------------------
* Allow Registration
* --------------------------------------------------------------------
* Determines whether users can register for the site.
*/
public bool $allowRegistration = true;
/**
* --------------------------------------------------------------------
* Welcome Link Lifetime
* --------------------------------------------------------------------
* Specifies the amount of time, in seconds, that a welcome link is valid.
* You can use Time Constants or any desired number.
*/
public int $welcomeLinkLifetime = 48 * HOUR;
/**
* --------------------------------------------------------------------
* User Provider
* --------------------------------------------------------------------
* The name of the class that handles user persistence.
* By default, this is the included UserModel, which
* works with any of the database engines supported by CodeIgniter.
* You can change it as long as they adhere to the
* CodeIgniter\Shield\Models\UserModel.
*
* @var class-string<UserModel>
*/
public string $userProvider = UserModel::class;
/**
* --------------------------------------------------------------------------
@ -52,4 +105,28 @@ class Auth extends MythAuthConfig
* Defines a base route for all authentication related pages
*/
public string $gateway = 'cp-auth';
public function __construct()
{
$adminGateway = config('Admin')
->gateway;
$this->redirects = [
'register' => $adminGateway,
'login' => $adminGateway,
'logout' => $adminGateway,
];
}
/**
* Returns the URL that a user should be redirected to after a successful login.
*
* Redirects to the set-password form if magicLogin
*/
public function loginRedirect(): string
{
$url = session('magicLogin') ? route_to('magic-link-set-password') : setting('Auth.redirects')['login'];
return $this->getUrl($url);
}
}

View File

@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Config;
use App\Models\PodcastModel;
use CodeIgniter\Shield\Config\AuthGroups as ShieldAuthGroups;
class AuthGroups extends ShieldAuthGroups
{
/**
* --------------------------------------------------------------------
* Default Group
* --------------------------------------------------------------------
* The group that a newly registered user is added to.
*/
public string $defaultGroup = 'podcaster';
/**
* --------------------------------------------------------------------
* Most powerful Group
* --------------------------------------------------------------------
* The group that a has the most permissions.
*/
public string $mostPowerfulGroup = 'superadmin';
/**
* --------------------------------------------------------------------
* Default Podcast Group
* --------------------------------------------------------------------
* The group that a newly registered user is added to.
*/
public string $defaultPodcastGroup = 'guest';
/**
* --------------------------------------------------------------------
* Most powerful Podcast Group
* --------------------------------------------------------------------
* The group that a has the most permissions on a podcast.
*/
public string $mostPowerfulPodcastGroup = 'admin';
/**
* --------------------------------------------------------------------
* Groups
* --------------------------------------------------------------------
* The available authentication systems, listed
* with alias and class name. These can be referenced
* by alias in the auth helper:
* auth('api')->attempt($credentials);
*
* @var array<string, array<string, string>>
*/
public array $groups = [];
/**
* --------------------------------------------------------------------
* Permissions
* --------------------------------------------------------------------
* The available permissions in the system. Each system is defined
* where the key is the
*
* If a permission is not listed here it cannot be used.
*
* @var array<string, string>
*/
public array $permissions = [];
/**
* --------------------------------------------------------------------
* Permissions Matrix
* --------------------------------------------------------------------
* Maps permissions to groups.
* @var array<string, array<string>>
*/
public array $matrix = [];
/**
* @var array<string, array<string, string>>
*/
public array $instanceGroups = [];
/**
* @var array<string, string>
*/
public array $instancePermissions = [];
/**
* @var array<string, array<string, string>>
*/
public array $podcastGroups = [];
/**
* @var array<string, string>
*/
public array $podcastPermissions = [];
/**
* @var string[]
*/
public array $instanceBaseGroups = ['superadmin', 'manager', 'podcaster'];
/**
* @var string[]
*/
public array $instanceBasePermissions = [
'admin.access',
'admin.settings',
'users.manage',
'persons.manage',
'pages.manage',
'podcasts.view',
'podcasts.create',
'podcasts.import',
'fediverse.manage-blocks',
];
/**
* @var array<string, array<string>>
*/
public array $instanceMatrix = [
'superadmin' => [
'admin.*',
'podcasts.*',
'users.manage',
'persons.manage',
'pages.manage',
'fediverse.manage-blocks',
],
'manager' => ['podcasts.create', 'podcasts.import', 'persons.manage', 'pages.manage'],
'podcaster' => ['admin.access'],
];
/**
* @var string[]
*/
public array $podcastBaseGroups = ['admin', 'editor', 'author', 'guest'];
/**
* @var string[]
*/
public array $podcastBasePermissions = [
'view',
'edit',
'delete',
'manage-import',
'manage-persons',
'manage-subscriptions',
'manage-contributors',
'manage-platforms',
'manage-publications',
'interact-as',
'episodes.view',
'episodes.create',
'episodes.edit',
'episodes.delete',
'episodes.manage-persons',
'episodes.manage-clips',
'episodes.manage-publications',
'episodes.manage-comments',
];
/**
* @var array<string, string[]>
*/
public array $podcastMatrix = [
'admin' => ['*'],
'editor' => [
'view',
'edit',
'manage-import',
'manage-persons',
'manage-platforms',
'manage-publications',
'interact-as',
'episodes.view',
'episodes.create',
'episodes.edit',
'episodes.delete',
'episodes.manage-persons',
'episodes.manage-clips',
'episodes.manage-publications',
'episodes.manage-comments',
],
'author' => [
'view',
'manage-persons',
'episodes.view',
'episodes.create',
'episodes.edit',
'episodes.manage-persons',
'episodes.manage-clips',
],
'guest' => ['view', 'episodes.view'],
];
/**
* Fill groups, permissions and matrix based on
*/
public function __construct($locale = null)
{
parent::__construct();
foreach ($this->instanceBaseGroups as $group) {
$this->instanceGroups[$group] = [
'title' => lang("Auth.instance_groups.{$group}.title"),
'description' => lang("Auth.instance_groups.{$group}.description"),
];
}
$this->groups = $this->instanceGroups;
foreach ($this->instanceBasePermissions as $permission) {
$this->instancePermissions[$permission] = lang("Auth.instance_permissions.{$permission}");
$this->permissions[$permission] = lang("Auth.instance_permissions.{$permission}");
}
$this->matrix = $this->instanceMatrix;
$this->generateBasePodcastAuthorizations();
/**
* For each podcast, include podcast groups, permissions, and matrix into $groups, $permissions, and $matrix
* attributes.
*/
$podcasts = (new PodcastModel())->findAll();
foreach ($podcasts as $podcast) {
$this->generatePodcastAuthorizations($podcast->id, $locale);
}
}
public function generateBasePodcastAuthorizations(): void
{
foreach ($this->podcastBaseGroups as $group) {
$this->podcastGroups[$group] = [
'title' => lang("Auth.podcast_groups.{$group}.title", [
'id' => '{id}',
]),
'description' => lang("Auth.podcast_groups.{$group}.description", [
'id' => '{id}',
]),
];
}
foreach ($this->podcastBasePermissions as $permission) {
$this->podcastPermissions[$permission] = lang("Auth.podcast_permissions.{$permission}", [
'id' => '{id}',
]);
$this->permissions[$permission] = lang("Auth.podcast_permissions.{$permission}", [
'id' => '{id}',
]);
}
}
public function generatePodcastAuthorizations(int $podcastId): void
{
foreach ($this->podcastBaseGroups as $group) {
$podcastGroup = 'podcast#' . $podcastId . '-' . $group;
$this->groups[$podcastGroup] = [
'title' => lang("Auth.podcast_groups.{$group}.title", [
'id' => $podcastId,
]),
'description' => lang("Auth.podcast_groups.{$group}.description", [
'id' => $podcastId,
]),
];
}
foreach ($this->podcastBasePermissions as $permission) {
$podcastPermission = 'podcast#' . $podcastId . '.' . $permission;
$this->permissions[$podcastPermission] = lang("Auth.podcast_permissions.{$permission}", [
'id' => $podcastId,
]);
}
foreach ($this->podcastMatrix as $group => $permissionWildcards) {
$podcastGroup = 'podcast#' . $podcastId . '-' . $group;
foreach ($permissionWildcards as $permissionWildcard) {
$podcastPermissionWildcard = 'podcast#' . $podcastId . '.' . $permissionWildcard;
$this->matrix[$podcastGroup][] = $podcastPermissionWildcard;
}
}
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Config;
use CodeIgniter\Shield\Config\AuthRoutes as ShieldAuthRoutes;
class AuthRoutes extends ShieldAuthRoutes
{
public array $routes = [
'register' => [
['get', 'register', 'RegisterController::registerView', 'register'],
['post', 'register', 'RegisterController::registerAction'],
],
'login' => [
['get', 'login', 'LoginController::loginView', 'login'],
['post', 'login', 'LoginController::loginAction'],
],
'magic-link' => [
[
'get',
'login/magic-link',
'MagicLinkController::loginView',
'magic-link', // Route name
],
['post', 'login/magic-link', 'MagicLinkController::loginAction'],
[
'get',
'login/verify-magic-link',
'MagicLinkController::verify',
'verify-magic-link', // Route name
],
],
'logout' => [['get', 'logout', 'LoginController::logoutAction', 'logout']],
'auth-actions' => [
['get', 'auth/a/show', 'ActionController::show', 'auth-action-show'],
['post', 'auth/a/handle', 'ActionController::handle', 'auth-action-handle'],
['post', 'auth/a/verify', 'ActionController::verify', 'auth-action-verify'],
],
];
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Config;
use CodeIgniter\Events\Events;
use CodeIgniter\Shield\Entities\User;
Events::on('logout', static function (User $user): void {
helper('auth');
// remove user's interact_as_actor session
remove_interact_as_actor();
});

View File

@ -6,52 +6,134 @@ namespace Modules\Auth\Config;
$routes = service('routes');
/**
* Overwriting Myth:auth routes file
*/
service('auth')
->routes($routes);
// Admin routes for users and podcast contributors
$routes->group(
config('Auth')
config('Admin')
->gateway,
[
'namespace' => 'Modules\Auth\Controllers',
],
static function ($routes): void {
// Login/out
$routes->get('login', 'AuthController::login', [
'as' => 'login',
$routes->get('magic-link-set-password', 'MagicLinkController::setPasswordView', [
'as' => 'magic-link-set-password',
]);
$routes->post('login', 'AuthController::attemptLogin');
$routes->get('logout', 'AuthController::logout', [
'as' => 'logout',
]);
// Registration
$routes->get('register', 'AuthController::register', [
'as' => 'register',
]);
$routes->post('register', 'AuthController::attemptRegister');
// Activation
$routes->get('activate-account', 'AuthController::activateAccount', [
'as' => 'activate-account',
]);
$routes->get(
'resend-activate-account',
'AuthController::resendActivateAccount',
[
'as' => 'resend-activate-account',
],
);
// Forgot/Resets
$routes->get('forgot', 'AuthController::forgotPassword', [
'as' => 'forgot',
]);
$routes->post('forgot', 'AuthController::attemptForgot');
$routes->get('reset-password', 'AuthController::resetPassword', [
'as' => 'reset-password',
]);
$routes->post('reset-password', 'AuthController::attemptReset');
// interacting as an actor
$routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
$routes->post('magic-link-set-password', 'MagicLinkController::setPasswordAction');
$routes->post('interact-as-actor', 'InteractController::attemptInteractAsActor', [
'as' => 'interact-as-actor',
]);
// Users
$routes->group('users', static function ($routes): void {
$routes->get('/', 'UserController::list', [
'as' => 'user-list',
'filter' => 'permission:users.manage',
]);
$routes->get('new', 'UserController::create', [
'as' => 'user-create',
'filter' => 'permission:users.manage',
]);
$routes->post('new', 'UserController::attemptCreate', [
'filter' => 'permission:users.manage',
]);
// User
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'UserController::view/$1', [
'as' => 'user-view',
'filter' => 'permission:users.manage',
]);
$routes->get('edit', 'UserController::edit/$1', [
'as' => 'user-edit',
'filter' => 'permission:users.manage',
]);
$routes->post('edit', 'UserController::attemptEdit/$1', [
'filter' => 'permission:users.manage',
]);
$routes->get('delete', 'UserController::delete/$1', [
'as' => 'user-delete',
'filter' => 'permission:users.manage',
]);
$routes->post('delete', 'UserController::attemptDelete/$1', [
'as' => 'user-delete',
'filter' => 'permission:users.manage',
]);
});
});
// My account
$routes->group('my-account', static function ($routes): void {
$routes->get('/', 'MyAccountController', [
'as' => 'my-account',
]);
$routes->get('change-password', 'MyAccountController::changePassword', [
'as' => 'change-password',
],);
$routes->post('change-password', 'MyAccountController::attemptChange');
});
// Podcast contributors
$routes->group('podcasts/(:num)/contributors', static function ($routes): void {
$routes->get('/', 'ContributorController::list/$1', [
'as' => 'contributor-list',
'filter' =>
'permission:podcast#.manage-contributors',
]);
$routes->get('add', 'ContributorController::add/$1', [
'as' => 'contributor-add',
'filter' => 'permission:podcast#.manage-contributors',
]);
$routes->post(
'add',
'ContributorController::attemptAdd/$1',
[
'filter' =>
'permission:podcast#.manage-contributors',
],
);
// Contributor
$routes->group('(:num)', static function ($routes): void {
$routes->get('/', 'ContributorController::view/$1/$2', [
'as' => 'contributor-view',
'filter' =>
'permission:podcast#.manage-contributors',
]);
$routes->get(
'edit',
'ContributorController::edit/$1/$2',
[
'as' => 'contributor-edit',
'filter' =>
'permission:podcast#.manage-contributors',
],
);
$routes->post(
'edit',
'ContributorController::attemptEdit/$1/$2',
[
'filter' =>
'permission:podcast#.manage-contributors',
],
);
$routes->get(
'remove',
'ContributorController::remove/$1/$2',
[
'as' => 'contributor-remove',
'filter' =>
'permission:podcast#.manage-contributors',
],
);
$routes->post(
'remove',
'ContributorController::attemptRemove/$1/$2',
[
'filter' =>
'permission:podcast#.manage-contributors',
],
);
});
});
}
);

View File

@ -4,87 +4,23 @@ declare(strict_types=1);
namespace Modules\Auth\Config;
use App\Models\UserModel;
use CodeIgniter\Config\BaseService;
use CodeIgniter\Model;
use Modules\Auth\Authorization\FlatAuthorization;
use Modules\Auth\Authorization\GroupModel;
use Modules\Auth\Authorization\PermissionModel;
use Myth\Auth\Models\LoginModel;
use CodeIgniter\Shield\Authentication\Authentication;
use Config\Services as BaseService;
use Modules\Auth\Auth;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses to do its job. This is used by CodeIgniter to allow
* the core of the framework to be swapped out easily without affecting the usage within the rest of your application.
*
* This file holds any application-specific services, or service overrides that you might need. An example has been
* included with the general method format you should use for your service methods. For more examples, see the core
* Services file at system/Config/Services.php.
*/
class Services extends BaseService
{
/**
* @return mixed
* The base auth class
*/
public static function authentication(
string $lib = 'local',
Model $userModel = null,
Model $loginModel = null,
bool $getShared = true
) {
public static function auth(bool $getShared = true): Auth
{
if ($getShared) {
return self::getSharedInstance('authentication', $lib, $userModel, $loginModel);
return self::getSharedInstance('auth');
}
// config() checks first in app/Config
$config = config('Auth');
$class = $config->authenticationLibs[$lib];
$instance = new $class($config);
if ($userModel === null) {
$userModel = new UserModel();
}
if ($loginModel === null) {
$loginModel = new LoginModel();
}
return $instance->setUserModel($userModel)
->setLoginModel($loginModel);
}
/**
* @return mixed|$this
*/
public static function authorization(
Model $groupModel = null,
Model $permissionModel = null,
Model $userModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance('authorization', $groupModel, $permissionModel, $userModel);
}
if ($groupModel === null) {
$groupModel = new GroupModel();
}
if ($permissionModel === null) {
$permissionModel = new PermissionModel();
}
/* @phpstan-ignore-next-line */
$instance = new FlatAuthorization($groupModel, $permissionModel);
if ($userModel === null) {
$userModel = new UserModel();
}
/* @phpstan-ignore-next-line */
return $instance->setUserModel($userModel);
return new Auth(new Authentication($config));
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Controllers;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Controllers\ActionController as ShieldActionController;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Class ActionController
*
* A generic controller to handle Authentication Actions.
*/
class ActionController extends ShieldActionController
{
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
parent::initController($request, $response, $logger);
Theme::setTheme('auth');
}
}

View File

@ -1,204 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Controllers;
use CodeIgniter\HTTP\RedirectResponse;
use Modules\Auth\Entities\User;
use Myth\Auth\Controllers\AuthController as MythAuthController;
use ViewThemes\Theme;
class AuthController extends MythAuthController
{
/**
* An array of helpers to be automatically loaded upon class instantiation.
*
* @var string[]
*/
protected $helpers = ['components'];
public function __construct()
{
parent::__construct();
Theme::setTheme('auth');
}
/**
* Attempt to register a new user.
*/
public function attemptRegister(): RedirectResponse
{
// Check if registration is allowed
if (! $this->config->allowRegistration) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.registerDisabled'));
}
$users = model('UserModel');
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'username' =>
'required|alpha_numeric_space|min_length[3]|is_unique[users.username]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
// Save the user
$allowedPostFields = array_merge(['password'], $this->config->validFields, $this->config->personalFields);
$user = new User($this->request->getPost($allowedPostFields));
$this->config->requireActivation === null
? $user->activate()
: $user->generateActivateHash();
// Ensure default group gets assigned if set
if ($this->config->defaultUserGroup !== null) {
$users = $users->withGroup($this->config->defaultUserGroup);
}
if (! $users->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
if ($this->config->requireActivation !== null) {
$activator = service('activator');
$sent = $activator->send($user);
if (! $sent) {
return redirect()
->back()
->withInput()
->with('error', $activator->error() ?? lang('Auth.unknownError'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.activationSuccess'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.registerSuccess'));
}
/**
* Verifies the code with the email and saves the new password, if they all pass validation.
*/
public function attemptReset(): RedirectResponse
{
if ($this->config->activeResetter === null) {
return redirect()
->route('login')
->with('error', lang('Auth.forgotDisabled'));
}
$users = model('UserModel');
// First things first - log the reset attempt.
$users->logResetAttempt(
$this->request->getPost('email'),
$this->request->getPost('token'),
$this->request->getIPAddress(),
(string) $this->request->getUserAgent(),
);
$rules = [
'token' => 'required',
'email' => 'required|valid_email',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
$user = $users
->where('email', $this->request->getPost('email'))
->where('reset_hash', $this->request->getPost('token'))
->first();
if ($user === null) {
return redirect()
->back()
->with('error', lang('Auth.forgotNoUser'));
}
// Reset token still valid?
if (
$user->reset_expires !== null &&
time() > $user->reset_expires->getTimestamp()
) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.resetTokenExpired'));
}
// Success! Save the new password, and cleanup the reset hash.
$user->password = $this->request->getPost('password');
$user->reset_hash = null;
$user->reset_at = date('Y-m-d H:i:s');
$user->reset_expires = null;
$user->force_pass_reset = false;
$users->save($user);
helper('auth');
// set interact_as_actor_id value
$userPodcasts = $user->podcasts;
if ($userPodcasts = $user->podcasts) {
set_interact_as_actor($userPodcasts[0]->actor_id);
}
return redirect()
->route('login')
->with('message', lang('Auth.resetSuccess'));
}
public function attemptInteractAsActor(): RedirectResponse
{
$rules = [
'actor_id' => 'required|numeric',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
helper('auth');
set_interact_as_actor((int) $this->request->getPost('actor_id'));
return redirect()->back();
}
}

View File

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Controllers;
use App\Entities\Podcast;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\Shield\Entities\User;
use Modules\Admin\Controllers\BaseController;
use Modules\Auth\Models\UserModel;
class ContributorController extends BaseController
{
protected Podcast $podcast;
protected ?User $contributor;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
if (($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) <= 1) {
return $this->{$method}();
}
if (($this->contributor = (new UserModel())->getPodcastContributor(
(int) $params[1],
(int) $params[0]
)) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
]);
return view('contributor/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
'contributor' => (new UserModel())->getPodcastContributor($this->contributor->id, $this->podcast->id),
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
1 => $this->contributor->username,
]);
return view('contributor/view', $data);
}
public function add(): string
{
helper('form');
$users = (new UserModel())->findAll();
$contributorOptions = array_reduce(
$users,
static function ($result, $user) {
$result[$user->id] = $user->username;
return $result;
},
[],
);
$roles = setting('AuthGroups.podcastBaseGroups');
$roleOptions = [];
array_walk(
$roles,
static function ($role, $key) use (&$roleOptions): array {
$roleOptions[$role] = lang('Auth.podcast_groups.' . $role . '.title');
return $roleOptions;
},
[],
);
$data = [
'podcast' => $this->podcast,
'contributorOptions' => $contributorOptions,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
]);
return view('contributor/add', $data);
}
public function attemptAdd(): RedirectResponse
{
$user = (new UserModel())->find((int) $this->request->getPost('user'));
if (get_podcast_group($user, $this->podcast->id)) {
return redirect()
->back()
->withInput()
->with('errors', [lang('Contributor.messages.alreadyAddedError')]);
}
add_podcast_group($user, $this->podcast->id, $this->request->getPost('role'));
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function edit(): string|RedirectResponse
{
helper('form');
$roles = setting('AuthGroups.podcastBaseGroups');
$roleOptions = [];
array_walk(
$roles,
static function ($role) use (&$roleOptions): array {
$roleOptions[$role] = lang('Auth.podcast_groups.' . $role . '.title');
return $roleOptions;
},
[],
);
$contributorGroup = get_podcast_group($this->contributor, $this->podcast->id);
if ($contributorGroup === null) {
return redirect()
->back()
->withInput()
->with('errors', [lang('Contributor.messages.notAddedError')]);
}
$data = [
'podcast' => $this->podcast,
'contributor' => $this->contributor,
'contributorGroup' => $contributorGroup,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
1 => $this->contributor->username,
]);
return view('contributor/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
// forbid updating a podcast owner
if ($this->podcast->created_by === $this->contributor->id) {
return redirect()
->back()
->with('errors', [lang('Contributor.messages.editOwnerError')]);
}
$group = $this->request->getPost('role');
set_podcast_group($this->contributor, $this->podcast->id, $group);
cache()
->delete("podcast#{$this->podcast->id}_contributors");
return redirect()->route('contributor-list', [$this->podcast->id])->with(
'message',
lang('Contributor.messages.editSuccess')
);
}
public function remove(): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
'contributor' => $this->contributor,
];
replace_breadcrumb_params([
0 => $this->podcast->at_handle,
1 => $this->contributor->username,
]);
return view('contributor/delete', $data);
}
public function attemptRemove(): RedirectResponse
{
if ($this->podcast->created_by === $this->contributor->id) {
return redirect()
->back()
->with('errors', [lang('Contributor.messages.removeOwnerError')]);
}
$rules = [
'understand' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
cache()
->delete("podcast#{$this->podcast->id}_contributors");
// remove contributor from podcast group
$this->contributor->removeGroup(get_podcast_group($this->contributor, $this->podcast->id));
return redirect()
->route('contributor-list', [$this->podcast->id])
->with(
'message',
lang('Contributor.messages.removeSuccess', [
'username' => $this->contributor->username,
'podcastTitle' => $this->podcast->title,
]),
);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RedirectResponse;
/**
* Class ActionController
*
* A generic controller to handle Authentication Actions.
*/
class InteractController extends Controller
{
public function attemptInteractAsActor(): RedirectResponse
{
$rules = [
'actor_id' => 'required|numeric',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
helper('auth');
set_interact_as_actor((int) $this->request->getPost('actor_id'));
return redirect()->back();
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Controllers;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Controllers\LoginController as ShieldLoginController;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
class LoginController extends ShieldLoginController
{
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
parent::initController($request, $response, $logger);
Theme::setTheme('auth');
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Controllers;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Controllers\MagicLinkController as ShieldMagicLinkController;
use Modules\Auth\Models\UserModel;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Handles "Magic Link" logins - an email-based no-password login protocol. This works much like password reset would,
* but Shield provides this in place of password reset. It can also be used on it's own without an email/password login
* strategy.
*/
class MagicLinkController extends ShieldMagicLinkController
{
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
parent::initController($request, $response, $logger);
Theme::setTheme('auth');
}
public function setPasswordView(): string | RedirectResponse
{
if (! session('magicLogin')) {
return redirect()->to(config('Auth')->loginRedirect());
}
return view(setting('Auth.views')['magic-link-set-password']);
}
public function setPasswordAction(): RedirectResponse
{
$rules = [
'new_password' => 'required|strong_password',
];
$userModel = new UserModel();
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// set new password to user
auth()
->user()
->password = $this->request->getPost('new_password');
if (! $userModel->update(auth()->user()->id, auth()->user())) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// remove magic login session to reinstate normal check
if (session('magicLogin')) {
session()->removeTempdata('magicLogin');
}
// Success!
return redirect()->to(config('Auth')->loginRedirect())
->with('message', lang('MyAccount.messages.passwordChangeSuccess'));
}
}

View File

@ -8,11 +8,11 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace Modules\Admin\Controllers;
namespace Modules\Auth\Controllers;
use App\Models\UserModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use Modules\Admin\Controllers\BaseController;
use Modules\Auth\Models\UserModel;
class MyAccountController extends BaseController
{
@ -30,16 +30,12 @@ class MyAccountController extends BaseController
public function attemptChange(): RedirectResponse
{
$auth = Services::authentication();
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'password' => 'required',
'new_password' => 'required|strong_password|differs[password]',
];
$userModel = new UserModel();
if (! $this->validate($rules)) {
return redirect()
->back()
@ -47,23 +43,28 @@ class MyAccountController extends BaseController
->with('errors', $userModel->errors());
}
// check credentials with the old password if logged in without magic link
$credentials = [
'email' => user()
'email' => auth()
->user()
->email,
'password' => $this->request->getPost('password'),
];
if (! $auth->validate($credentials)) {
return redirect()
->back()
->withInput()
$validCreds = auth()
->check($credentials);
if (! $validCreds->isOK()) {
return redirect()->back()
->with('error', lang('MyAccount.messages.wrongPasswordError'));
}
user()
// set new password to user
auth()
->user()
->password = $this->request->getPost('new_password');
if (! $userModel->update(user_id(), user())) {
if (! $userModel->update(auth()->user()->id, auth()->user())) {
return redirect()
->back()
->withInput()

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Modules\Auth\Controllers;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Controllers\RegisterController as ShieldRegisterController;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Class RegisterController
*
* Handles displaying registration form, and handling actual registration flow.
*/
class RegisterController extends ShieldRegisterController
{
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
parent::initController($request, $response, $logger);
Theme::setTheme('auth');
}
}

View File

@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Controllers;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Authentication\Authenticators\Session;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Shield\Exceptions\ValidationException;
use CodeIgniter\Shield\Models\UserIdentityModel;
use Modules\Admin\Controllers\BaseController;
use Modules\Auth\Models\UserModel;
class UserController extends BaseController
{
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
return $this->{$method}();
}
if ($this->user = (new UserModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'users' => (new UserModel())->findAll(),
];
return view('user/list', $data);
}
public function view(): string
{
$data = [
'user' => $this->user,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('user/view', $data);
}
public function create(): string
{
helper('form');
$roles = setting('AuthGroups.instanceGroups');
$roleOptions = [];
array_walk(
$roles,
static function ($role, $key) use (&$roleOptions): array {
$roleOptions[$key] = $role['title'];
return $roleOptions;
},
[],
);
$data = [
'roleOptions' => $roleOptions,
];
return view('user/create', $data);
}
/**
* Create the user with the provided username and email. The password is set as a random string and a magic link is
* sent to the user to allow them setting their password.
*/
public function attemptCreate(): RedirectResponse
{
helper('text');
$db = db_connect();
$db->transStart();
$userModel = new UserModel();
// Save the user
$email = $this->request->getPost('email');
$user = new User([
'username' => $this->request->getPost('username'),
'email' => $email,
// set a random password
// user will be prompted to change it on first magic link login.
'password' => random_string('alnum', 32),
]);
try {
$userModel->save($user);
} catch (ValidationException) {
return redirect()->back()
->withInput()
->with('errors', $userModel->errors());
}
$user = $userModel->findById($userModel->getInsertID());
$user->addGroup($this->request->getPost('role'));
// **** SEND WELCOME LINK FOR FIRST LOGIN ****
/** @var UserIdentityModel $identityModel */
$identityModel = model(UserIdentityModel::class);
// Delete any previous magic-link identities
$identityModel->deleteIdentitiesByType($user, Session::ID_TYPE_MAGIC_LINK);
// Generate the code and save it as an identity
$token = random_string('crypto', 20);
$identityModel->insert([
'user_id' => $user->id,
'type' => Session::ID_TYPE_MAGIC_LINK,
'secret' => $token,
'expires' => Time::now()->addSeconds(setting('Auth.welcomeLinkLifetime'))->format('Y-m-d H:i:s'),
]);
// Send the user an email with the code
$email = emailer()
->setFrom(setting('Email.fromEmail'), setting('Email.fromName') ?? '');
$email->setTo($user->email);
$email->setSubject(lang('Auth.welcomeSubject', [
'siteName' => setting('App.siteName'),
], null, false));
$email->setMessage(view(setting('Auth.views')['welcome-email'], [
'token' => $token,
], [
'theme' => 'auth',
]));
if (! $email->send(false)) {
log_message('error', $email->printDebugger(['headers']));
return redirect()->back()
->with('error', lang('Auth.unableSendEmailToUser', [$user->email]));
}
// Clear the email
$email->clear();
$db->transComplete();
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.createSuccess', [
'username' => $user->username,
]));
}
public function edit(): string
{
helper('form');
$roles = setting('AuthGroups.instanceGroups');
$roleOptions = [];
array_walk(
$roles,
static function ($role, $key) use (&$roleOptions): array {
$roleOptions[$key] = $role['title'];
return $roleOptions;
},
[],
);
$data = [
'user' => $this->user,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('user/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
// The instance owner is a superadmin and the only user that cannot be demoted.
if ((bool) $this->user->is_owner) {
return redirect()
->back()
->with('errors', [
lang('User.messages.editOwnerError', [
'username' => $this->user->username,
]),
]);
}
$group = $this->request->getPost('role');
set_instance_group($this->user, $group);
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.roleEditSuccess', [
'username' => $this->user->username,
]));
}
public function delete(): string
{
helper(['form']);
$data = [
'user' => $this->user,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('user/delete', $data);
}
public function attemptDelete(): RedirectResponse
{
// You cannot delete the instance owner.
if ((bool) $this->user->is_owner) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteOwnerError', [
'username' => $this->user->username,
]),
]);
}
// You cannot delete a superadmin
// superadmin has to be demoted before being deleted
if ($this->user->inGroup(setting('AuthGroups.mostPowerfulPodcastGroup'))) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
$rules = [
'understand' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new UserModel())->delete($this->user->id, true);
return redirect()
->route('user-list')
->with('message', lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
]));
}
}

View File

@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastUsers Creates podcast_users table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsUsers extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'user_id' => [
'type' => 'INT',
'unsigned' => true,
],
'group_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id', '', 'CASCADE');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('group_id', 'auth_groups', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_users');
}
public function down(): void
{
$this->forge->dropTable('podcasts_users');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
// Add custom column for shield
class AddCustomColumnForUser extends Migration
{
public function up(): void
{
$fields = [
'is_owner' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'null' => false,
],
];
$this->forge->addColumn('users', $fields);
}
public function down(): void
{
$fields = ['is_owner'];
$this->forge->dropColumn('users', $fields);
}
}

View File

@ -1,302 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PermissionSeeder Inserts permissions
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Database\Seeds;
use CodeIgniter\Database\Seeder;
class AuthSeeder extends Seeder
{
/**
* @var array<string, string>[]
*/
protected array $groups = [
[
'name' => 'superadmin',
'description' =>
'Somebody who has access to all the castopod instance features',
],
[
'name' => 'podcast_admin',
'description' =>
'Somebody who has access to all the features within a given podcast',
],
];
/**
* Build permissions array as a list of:
*
* ``` context => [ [action, description], [action, description], ... ] ```
*
* @var array<string, array<string, string|array>[]>
*/
protected array $permissions = [
'users' => [
[
'name' => 'create',
'description' => 'Create a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all users',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any user info',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_authorizations',
'description' => 'Add or remove roles/permissions to a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_bans',
'description' => 'Ban / unban a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'force_pass_reset',
'description' =>
'Force a user to update his password upon next login',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete user without removing him from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of a user from the database',
'has_permission' => ['superadmin'],
],
],
'pages' => [
[
'name' => 'manage',
'description' => 'List / create / edit / delete pages',
'has_permission' => ['superadmin'],
],
],
'podcasts' => [
[
'name' => 'create',
'description' => 'Add a new podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'import',
'description' => 'Import a new podcast from an external feed',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all podcasts and their episodes',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any podcast and their contributors list',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' => 'Delete any podcast from the database',
'has_permission' => ['superadmin'],
],
],
'episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of any podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any episode of any podcast',
'has_permission' => ['superadmin'],
],
],
'podcast' => [
[
'name' => 'view',
'description' => 'View a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_contributors',
'description' =>
'Add / remove contributors to a podcast and edit their roles',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_platforms',
'description' => 'Set / remove platform links of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_publications',
'description' =>
'Publish / unpublish episodes & posts of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'description' =>
'Interact as the podcast to favourite / share or reply to posts.',
'has_permission' => ['podcast_admin'],
],
],
'podcast_episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'view',
'description' => 'View any episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'create',
'description' => 'Add new episodes for a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit an episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete',
'description' =>
'Delete all occurrences of an episode of a podcast from the database',
'has_permission' => ['podcast_admin'],
],
],
'person' => [
[
'name' => 'create',
'description' => 'Add a new person',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all persons',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any person',
'has_permission' => ['superadmin'],
],
[
'name' => 'edit',
'description' => 'Edit a person',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete permanently any person from the database',
'has_permission' => ['superadmin'],
],
],
'fediverse' => [
[
'name' => 'block_actors',
'description' =>
'Block fediverse actors from interacting with the instance.',
'has_permission' => ['superadmin'],
],
[
'name' => 'block_domains',
'description' =>
'Block fediverse domains from interacting with the instance.',
'has_permission' => ['superadmin'],
],
],
];
public function run(): void
{
$groupId = 0;
$dataGroups = [];
foreach ($this->groups as $group) {
$dataGroups[] = [
'id' => ++$groupId,
'name' => $group['name'],
'description' => $group['description'],
];
}
// Map permissions to a format the `auth_permissions` table expects
$dataPermissions = [];
$dataGroupsPermissions = [];
$permissionId = 0;
foreach ($this->permissions as $context => $actions) {
foreach ($actions as $action) {
$dataPermissions[] = [
'id' => ++$permissionId,
'name' => $context . '-' . $action['name'],
'description' => $action['description'],
];
foreach ($action['has_permission'] as $role) {
// link permission to specified groups
$dataGroupsPermissions[] = [
'group_id' => $this->getGroupIdByName($role, $dataGroups),
'permission_id' => $permissionId,
];
}
}
}
$this->db
->table('auth_permissions')
->ignore(true)
->insertBatch($dataPermissions);
$this->db
->table('auth_groups')
->ignore(true)
->insertBatch($dataGroups);
$this->db
->table('auth_groups_permissions')
->ignore(true)
->insertBatch($dataGroupsPermissions);
}
/**
* @param array<string, string|int>[] $dataGroups
*/
public static function getGroupIdByName(string $name, array $dataGroups): ?int
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
}

View File

@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Auth\Entities;
use App\Entities\Podcast;
use App\Models\PodcastModel;
use App\Models\UserModel;
use Modules\Fediverse\Models\NotificationModel;
use Myth\Auth\Entities\User as MythAuthUser;
use RuntimeException;
/**
* @property int $id
* @property string $username
* @property string $email
* @property string $password
* @property bool $active
* @property bool $force_pass_reset
* @property int|null $podcast_id
* @property string|null $podcast_role
*
* @property Podcast[] $podcasts All podcasts the user is contributing to
* @property int[] $actorIdsWithUnreadNotifications Ids of the user's actors that have unread notifications
*/
class User extends MythAuthUser
{
public bool $is_owner;
/**
* @var Podcast[]|null
*/
protected ?array $podcasts = null;
/**
* @var int[]|null
*/
protected ?array $actorIdsWithUnreadNotifications = null;
/**
* Array of field names and the type of value to cast them as when they are accessed.
*
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'active' => 'boolean',
'force_pass_reset' => 'boolean',
'podcast_id' => '?integer',
'podcast_role' => '?string',
];
public function getIsOwner(): bool
{
$firstUser = (new UserModel())->first();
if (! $firstUser instanceof self) {
return false;
}
return $this->username === $firstUser->username;
}
/**
* Returns the podcasts the user is contributing to
*
* @return Podcast[]
*/
public function getPodcasts(): array
{
if ($this->id === null) {
throw new RuntimeException('Users must be created before getting podcasts.');
}
if ($this->podcasts === null) {
$this->podcasts = (new PodcastModel())->getUserPodcasts($this->id);
}
return $this->podcasts;
}
/**
* Returns the ids of the user's actors that have unread notifications
*
* @return int[]
*/
public function getActorIdsWithUnreadNotifications(): array
{
if ($this->getPodcasts() === []) {
return [];
}
$unreadNotifications = (new NotificationModel())->whereIn(
'target_actor_id',
array_column($this->getPodcasts(), 'actor_id')
)
->where('read_at', null)
->findAll();
return array_column($unreadNotifications, 'target_actor_id');
}
}

View File

@ -8,8 +8,8 @@ use App\Models\PodcastModel;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Exceptions\RuntimeException;
use Config\Services;
use Myth\Auth\Exceptions\PermissionException;
class PermissionFilter implements FilterInterface
{
@ -24,62 +24,49 @@ class PermissionFilter implements FilterInterface
*/
public function before(RequestInterface $request, $params = null)
{
helper('auth');
if ($params === null) {
if (empty($params)) {
return;
}
$authenticate = Services::authentication();
// if no user is logged in then send to the login form
if (! $authenticate->check()) {
session()->set('redirect_url', current_url());
return redirect('login');
if (! function_exists('auth')) {
helper('auth');
}
helper('misc');
$authorize = Services::authorization();
$router = Services::router();
$routerParams = $router->params();
$result = false;
if (! auth()->loggedIn()) {
return redirect()->to('login');
}
$result = true;
// Check if user has at least one of the permissions
foreach ($params as $permission) {
// check if permission is for a specific podcast
if (
(str_starts_with($permission, 'podcast-') ||
str_starts_with($permission, 'podcast_episodes-')) &&
$routerParams !== []
) {
if (
($groupId = (new PodcastModel())->getContributorGroupId(
$authenticate->id(),
$routerParams[0],
)) &&
$authorize->groupHasPermission($permission, $groupId)
) {
$result = true;
break;
// does permission is specific to a podcast?
if (str_contains($permission, '#')) {
$router = Services::router();
$routerParams = $router->params();
// get podcast id
$podcastId = null;
if (is_numeric($routerParams[0])) {
$podcastId = (int) $routerParams[0];
} else {
$podcast = (new PodcastModel())->getPodcastByHandle($routerParams[0]);
if ($podcast !== null) {
$podcastId = $podcast->id;
}
}
if ($podcastId !== null) {
$permission = str_replace('#', '#' . $podcastId, $permission);
}
} elseif (
$authorize->hasPermission($permission, $authenticate->id())
) {
$result = true;
break;
}
$result = $result && auth()
->user()
->can($permission);
}
if (! $result) {
if ($authenticate->silent()) {
$redirectURL = session('redirect_url') ?? '/';
unset($_SESSION['redirect_url']);
return redirect()
->to($redirectURL)
->with('error', lang('Auth.notEnoughPrivilege'));
}
throw new PermissionException(lang('Auth.notEnoughPrivilege'));
throw new RuntimeException(lang('Auth.notEnoughPrivilege'), 403);
}
}

View File

@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
use App\Entities\Podcast;
use App\Models\ActorModel;
use App\Models\PodcastModel;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Auth;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Models\NotificationModel;
if (! function_exists('auth')) {
/**
* Provides convenient access to the main Auth class for CodeIgniter Shield.
*
* @param string|null $alias Authenticator alias
*/
function auth(?string $alias = null): Auth
{
/** @var Auth $auth */
$auth = service('auth');
return $auth->setAuthenticator($alias);
}
}
if (! function_exists('set_interact_as_actor')) {
/**
* Sets the actor id of which the user is acting as
*/
function set_interact_as_actor(int $actorId): void
{
if (auth()->loggedIn()) {
session()
->set('interact_as_actor_id', $actorId);
}
}
}
if (! function_exists('remove_interact_as_actor')) {
/**
* Removes the actor id of which the user is acting as
*/
function remove_interact_as_actor(): void
{
session()->remove('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor_id')) {
/**
* Sets the podcast id of which the user is acting as
*/
function interact_as_actor_id(): ?int
{
return session()->get('interact_as_actor_id');
}
}
if (! function_exists('interact_as_actor')) {
/**
* Get the actor the user is currently interacting as
*/
function interact_as_actor(): Actor | false
{
if (! auth()->loggedIn()) {
return false;
}
$session = session();
if (! $session->has('interact_as_actor_id')) {
return false;
}
return model(ActorModel::class, false)->getActorById($session->get('interact_as_actor_id'));
}
}
if (! function_exists('can_user_interact')) {
function can_user_interact(): bool
{
return (bool) interact_as_actor();
}
}
if (! function_exists('add_podcast_group')) {
function add_podcast_group(User $user, int $podcastId, string $group): User
{
$podcastGroup = 'podcast#' . $podcastId . '-' . $group;
return $user->addGroup($podcastGroup);
}
}
if (! function_exists('get_instance_group')) {
function get_instance_group(User $user): ?string
{
$instanceGroups = array_filter($user->getGroups() ?? [], static function ($group): bool {
return ! str_starts_with($group, 'podcast#');
});
if ($instanceGroups === []) {
return null;
}
$instanceGroup = array_shift($instanceGroups);
// Verify that a user belongs to one group only!
if ($instanceGroups !== []) {
// remove any other group the user belongs to
$user->removeGroup(...$instanceGroups);
}
return $instanceGroup;
}
}
if (! function_exists('set_instance_group')) {
function set_instance_group(User $user, string $group): User
{
// remove old instance group
if (get_instance_group($user)) {
$user->removeGroup(get_instance_group($user));
}
// set new group
return $user->addGroup($group);
}
}
if (! function_exists('get_podcast_group')) {
function get_podcast_group(User $user, int $podcastId): ?string
{
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group) use ($podcastId): bool {
return str_starts_with($group, "podcast#{$podcastId}");
});
if ($podcastGroups === []) {
return null;
}
$podcastGroup = array_shift($podcastGroups);
// Verify that a user belongs to one group only!
if ($podcastGroups !== []) {
// remove any other group the user belongs to
$user->removeGroup(...$podcastGroups);
}
// strip the `podcast#{id}.` prefix when returning group
return substr($podcastGroup, strlen('podcast#' . $podcastId . '-'));
}
}
if (! function_exists('set_podcast_group')) {
function set_podcast_group(User $user, int $podcastId, string $group): User
{
// remove old instance group
$user->removeGroup("podcast#{$podcastId}-" . get_podcast_group($user, $podcastId));
// set new group
return add_podcast_group($user, $podcastId, $group);
}
}
if (! function_exists('get_podcast_groups')) {
/**
* @return string[]
*/
function get_user_podcast_ids(User $user): array
{
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group): bool {
return str_starts_with($group, 'podcast#');
});
$userPodcastIds = [];
// extract all podcast ids from groups
foreach ($podcastGroups as $podcastGroup) {
$userPodcastIds[] = substr($podcastGroup, strpos($podcastGroup, '#') + 1, 1);
}
return $userPodcastIds;
}
}
if (! function_exists('can_podcast')) {
function can_podcast(User $user, int $podcastId, string $permission): bool
{
return $user->can('podcast#' . $podcastId . '.' . $permission);
}
}
if (! function_exists('get_user_podcasts')) {
/**
* Returns the podcasts the user is contributing to
*
* @return Podcast[]
*/
function get_user_podcasts(User $user): array
{
return (new PodcastModel())->getUserPodcasts($user->id, get_user_podcast_ids($user));
}
}
if (! function_exists('get_podcasts_user_can_interact_with')) {
/**
* @return Podcast[]
*/
function get_podcasts_user_can_interact_with(User $user): array
{
$userPodcasts = (new PodcastModel())->getUserPodcasts($user->id, get_user_podcast_ids($user));
$hasInteractAsPrivilege = interact_as_actor_id() === null;
if ($userPodcasts === []) {
if ($hasInteractAsPrivilege) {
remove_interact_as_actor();
}
return [];
}
$isInteractAsPrivilegeLost = true;
$podcastsUserCanInteractWith = [];
foreach ($userPodcasts as $userPodcast) {
if (can_podcast($user, $userPodcast->id, 'interact-as')) {
if (interact_as_actor_id() === $userPodcast->actor_id) {
$isInteractAsPrivilegeLost = false;
}
$podcastsUserCanInteractWith[] = $userPodcast;
}
}
if ($podcastsUserCanInteractWith === []) {
if (interact_as_actor_id() !== null) {
remove_interact_as_actor();
}
return [];
}
// check if user has lost the interact as privilege for current podcast actor.
// --> Remove interact as if there's no podcast actor to interact as
// or set the first podcast actor the user can interact as
if ($isInteractAsPrivilegeLost) {
set_interact_as_actor($podcastsUserCanInteractWith[0]->actor_id);
}
return $podcastsUserCanInteractWith;
}
}
if (! function_exists('get_actor_ids_with_unread_notifications')) {
/**
* Returns the ids of the user's actors that have unread notifications
*
* @return int[]
*/
function get_actor_ids_with_unread_notifications(User $user): array
{
if (($userPodcasts = get_user_podcasts($user)) === []) {
return [];
}
$unreadNotifications = (new NotificationModel())->whereIn(
'target_actor_id',
array_column($userPodcasts, 'actor_id')
)
->where('read_at', null)
->findAll();
return array_column($unreadNotifications, 'target_actor_id');
}
}
if (! function_exists('get_group_title')) {
/**
* @return array<'title'|'description', string>
*/
function get_group_info(string $group, ?int $podcastId = null): array
{
if ($podcastId === null) {
return setting('AuthGroups.instanceGroups')[$group];
}
return setting('AuthGroups.podcastGroups')[$group];
}
}

Some files were not shown because too many files have changed in this diff Show More