feat: initial migration to pennant features for feature flags and global site config [incremental] (#573)

* feat: initial commit for feature management

* style: fix StyleCI findings

* fix(api): prohibit features of nonnull scope from API & fix(admin): wrong translate key

* style: fix Static Analysis error
This commit is contained in:
paranarimasu authored and GitHub committed 2023-04-23 18:54:34 -05:00
1 parent 2e930b17ff
commit 088ea287c5
161 files changed
+2521 -1404

No files matched your search

+3 -10
View File
@@ -157,15 +157,6 @@ SCRIPT_STREAM_READS=
SCRIPT_DISABLE_ASSERTS=
SCRIPT_VISIBILITY=
# flags
ALLOW_VIDEO_STREAMS=false
ALLOW_AUDIO_STREAMS=false
ALLOW_DISCORD_NOTIFICATIONS=false
ALLOW_VIEW_RECORDING=false
ALLOW_DUMP_DOWNLOADING=false
ALLOW_SCRIPT_DOWNLOADING=false
ALLOW_PLAYLIST_MANAGEMENT=false
# fortify
FORTIFY_HOME=http://localhost
FORTIFY_PATH=
@@ -214,6 +205,9 @@ NOVA_LICENSE_KEY=
NOVA_PASSWORDS=null
NOVA_PATH=/admin
# pennant
PENNANT_STORE=database
# playlist
PLAYLIST_MAX_TRACKS=1000
USER_MAX_PLAYLISTS=1000
@@ -278,7 +272,6 @@ VIDEO_PATH=/video
VIDEO_URL=
VIDEO_STREAMING_METHOD=response
VIDEO_NGINX_REDIRECT=
VIDEO_ENCODER_VERSION=
SCRIPT_DISK=scripts_local
SCRIPT_DISK_ROOT=
+1 -2
View File
@@ -18,7 +18,6 @@ use Illuminate\Database\Console\PruneCommand as PruneModelsCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Queue\Console\MonitorCommand as MonitorQueueCommand;
use Illuminate\Queue\Console\PruneFailedJobsCommand;
use Illuminate\Support\Facades\Config;
use Laravel\Horizon\Console\SnapshotCommand;
use Laravel\Sanctum\Console\Commands\PruneExpired;
use Laravel\Telescope\Console\PruneCommand as PruneTelescopeEntriesCommand;
@@ -99,7 +98,7 @@ class Kernel extends ConsoleKernel
->storeOutput()
->hourly();
if (Config::bool('telescope.enabled')) {
if (config('telescope.enabled') === true) {
$schedule->command(PruneTelescopeEntriesCommand::class)
->withoutOverlapping()
->runInBackground()
-39
View File
@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class FlagConstants.
*/
class FlagConstants
{
final public const ALLOW_VIDEO_STREAMS_FLAG = 'allow_video_streams';
final public const ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED = 'flags.allow_video_streams';
final public const ALLOW_AUDIO_STREAMS_FLAG = 'allow_audio_streams';
final public const ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED = 'flags.allow_audio_streams';
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG = 'allow_discord_notifications';
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED = 'flags.allow_discord_notifications';
final public const ALLOW_VIEW_RECORDING_FLAG = 'allow_view_recording';
final public const ALLOW_VIEW_RECORDING_FLAG_QUALIFIED = 'flags.allow_view_recording';
final public const ALLOW_DUMP_DOWNLOADING_FLAG = 'allow_dump_downloading';
final public const ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_dump_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG = 'allow_script_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_script_downloading';
final public const ALLOW_PLAYLIST_MANAGEMENT = 'allow_playlist_management';
final public const ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED = 'flags.allow_playlist_management';
}
-4
View File
@@ -13,10 +13,6 @@ class VideoConstants
final public const DISKS_QUALIFIED = 'video.disks';
final public const ENCODER_VERSION = 'encoder_version';
final public const ENCODER_VERSION_QUALIFIED = 'video.encoder_version';
final public const NGINX_REDIRECT_QUALIFIED = 'video.nginx_redirect';
final public const PATH_QUALIFIED = 'video.path';
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Constants;
/**
* Class FeatureConstants.
*/
class FeatureConstants
{
final public const ALLOW_DISCORD_NOTIFICATIONS = 'allow_discord_notifications';
final public const ALLOW_VIEW_RECORDING = 'allow_view_recording';
final public const NULL_SCOPE = '__laravel_null';
final public const REQUIRED_ENCODER_VERSION = 'required_encoder_version';
}
@@ -24,4 +24,11 @@ interface DiscordMessageEvent
* @return string
*/
public function getDiscordChannel(): string;
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool;
}
@@ -17,19 +17,19 @@ interface NovaNotificationEvent
*
* @return bool
*/
public function shouldSend(): bool;
public function shouldSendNovaNotification(): bool;
/**
* Get the nova notification.
*
* @return NovaNotification
*/
public function getNotification(): NovaNotification;
public function getNovaNotification(): NovaNotification;
/**
* Get the users to notify.
*
* @return Collection
*/
public function getUsers(): Collection;
public function getNovaNotificationRecipients(): Collection;
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Feature;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class FeatureCreated.
*/
class FeatureCreated extends FeatureEvent
{
use Dispatchable;
use SerializesModels;
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$feature = $this->getFeature();
return DiscordMessage::create('', [
'description' => "Feature '**{$feature->getName()}**' has been created.",
'color' => EmbedColor::GREEN,
]);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Feature;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class FeatureDeleted.
*/
class FeatureDeleted extends FeatureEvent
{
use Dispatchable;
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$feature = $this->getFeature();
return DiscordMessage::create('', [
'description' => "Feature '**{$feature->getName()}**' has been deleted.",
'color' => EmbedColor::RED,
]);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Feature;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Models\Admin\Feature;
use Illuminate\Support\Facades\Config;
/**
* Class FeatureEvent.
*/
abstract class FeatureEvent implements DiscordMessageEvent
{
/**
* Create a new event instance.
*
* @param Feature $feature
* @return void
*/
public function __construct(protected Feature $feature)
{
}
/**
* Get the feature that has fired this event.
*
* @return Feature
*/
public function getFeature(): Feature
{
return $this->feature;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return $this->feature->isNullScope();
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Feature;
use App\Concerns\Discord\HasAttributeUpdateEmbedFields;
use App\Enums\Discord\EmbedColor;
use App\Models\Admin\Feature;
use Illuminate\Foundation\Events\Dispatchable;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class FeatureUpdated.
*/
class FeatureUpdated extends FeatureEvent
{
use Dispatchable;
use HasAttributeUpdateEmbedFields;
/**
* Create a new event instance.
*
* @param Feature $feature
* @return void
*/
public function __construct(Feature $feature)
{
parent::__construct($feature);
$this->initializeEmbedFields($feature);
}
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$feature = $this->getFeature();
return DiscordMessage::create('', [
'description' => "Feature '**{$feature->getName()}**' has been updated.",
'fields' => $this->getEmbedFields(),
'color' => EmbedColor::YELLOW,
]);
}
}
+1 -14
View File
@@ -4,18 +4,15 @@ declare(strict_types=1);
namespace App\Events\Admin\Setting;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class SettingCreated.
*/
class SettingCreated extends SettingEvent implements DiscordMessageEvent
class SettingCreated extends SettingEvent
{
use Dispatchable;
use SerializesModels;
@@ -34,14 +31,4 @@ class SettingCreated extends SettingEvent implements DiscordMessageEvent
'color' => EmbedColor::GREEN,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+1 -14
View File
@@ -4,17 +4,14 @@ declare(strict_types=1);
namespace App\Events\Admin\Setting;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class SettingDeleted.
*/
class SettingDeleted extends SettingEvent implements DiscordMessageEvent
class SettingDeleted extends SettingEvent
{
use Dispatchable;
@@ -32,14 +29,4 @@ class SettingDeleted extends SettingEvent implements DiscordMessageEvent
'color' => EmbedColor::RED,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+24 -1
View File
@@ -4,12 +4,15 @@ declare(strict_types=1);
namespace App\Events\Admin\Setting;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Models\Admin\Setting;
use Illuminate\Support\Facades\Config;
/**
* Class SettingEvent.
*/
abstract class SettingEvent
abstract class SettingEvent implements DiscordMessageEvent
{
/**
* Create a new event instance.
@@ -30,4 +33,24 @@ abstract class SettingEvent
{
return $this->setting;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
+1 -14
View File
@@ -5,18 +5,15 @@ declare(strict_types=1);
namespace App\Events\Admin\Setting;
use App\Concerns\Discord\HasAttributeUpdateEmbedFields;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use App\Models\Admin\Setting;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class SettingUpdated.
*/
class SettingUpdated extends SettingEvent implements DiscordMessageEvent
class SettingUpdated extends SettingEvent
{
use Dispatchable;
use HasAttributeUpdateEmbedFields;
@@ -48,14 +45,4 @@ class SettingUpdated extends SettingEvent implements DiscordMessageEvent
'color' => EmbedColor::YELLOW,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+1 -14
View File
@@ -4,18 +4,15 @@ declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserCreated.
*/
class UserCreated extends UserEvent implements DiscordMessageEvent
class UserCreated extends UserEvent
{
use Dispatchable;
use SerializesModels;
@@ -34,14 +31,4 @@ class UserCreated extends UserEvent implements DiscordMessageEvent
'color' => EmbedColor::GREEN,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+1 -14
View File
@@ -4,17 +4,14 @@ declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserDeleted.
*/
class UserDeleted extends UserEvent implements DiscordMessageEvent
class UserDeleted extends UserEvent
{
use Dispatchable;
@@ -32,14 +29,4 @@ class UserDeleted extends UserEvent implements DiscordMessageEvent
'color' => EmbedColor::RED,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+24 -1
View File
@@ -4,12 +4,15 @@ declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Models\Auth\User;
use Illuminate\Support\Facades\Config;
/**
* Class UserEvent.
*/
abstract class UserEvent
abstract class UserEvent implements DiscordMessageEvent
{
/**
* Create a new event instance.
@@ -30,4 +33,24 @@ abstract class UserEvent
{
return $this->user;
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
+1 -14
View File
@@ -4,18 +4,15 @@ declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserRestored.
*/
class UserRestored extends UserEvent implements DiscordMessageEvent
class UserRestored extends UserEvent
{
use Dispatchable;
use SerializesModels;
@@ -34,14 +31,4 @@ class UserRestored extends UserEvent implements DiscordMessageEvent
'color' => EmbedColor::GREEN,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+1 -14
View File
@@ -5,18 +5,15 @@ declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Concerns\Discord\HasAttributeUpdateEmbedFields;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use App\Models\Auth\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserUpdated.
*/
class UserUpdated extends UserEvent implements DiscordMessageEvent
class UserUpdated extends UserEvent
{
use Dispatchable;
use HasAttributeUpdateEmbedFields;
@@ -48,14 +45,4 @@ class UserUpdated extends UserEvent implements DiscordMessageEvent
'color' => EmbedColor::YELLOW,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
@@ -26,4 +26,14 @@ abstract class AdminCreatedEvent extends BaseCreatedEvent
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
@@ -26,4 +26,14 @@ abstract class AdminDeletedEvent extends BaseDeletedEvent
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
@@ -26,4 +26,14 @@ abstract class AdminRestoredEvent extends BaseRestoredEvent
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
@@ -26,4 +26,14 @@ abstract class AdminUpdatedEvent extends BaseUpdatedEvent
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
}
+10
View File
@@ -36,6 +36,16 @@ abstract class BaseCreatedEvent extends BaseEvent implements DiscordMessageEvent
]);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the description for the Discord message payload.
*
+10
View File
@@ -34,6 +34,16 @@ abstract class BaseDeletedEvent extends BaseEvent implements DiscordMessageEvent
]);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the description for the Discord message payload.
*
+10
View File
@@ -36,6 +36,16 @@ abstract class BaseRestoredEvent extends BaseEvent implements DiscordMessageEven
]);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the description for the Discord message payload.
*
+10
View File
@@ -37,6 +37,16 @@ abstract class BaseUpdatedEvent extends BaseEvent implements DiscordMessageEvent
]);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the description for the Discord message payload.
*
+5 -5
View File
@@ -37,7 +37,7 @@ abstract class WikiDeletedEvent extends BaseDeletedEvent implements NovaNotifica
*
* @return bool
*/
public function shouldSend(): bool
public function shouldSendNovaNotification(): bool
{
$model = $this->getModel();
@@ -49,13 +49,13 @@ abstract class WikiDeletedEvent extends BaseDeletedEvent implements NovaNotifica
*
* @return NovaNotification
*/
public function getNotification(): NovaNotification
public function getNovaNotification(): NovaNotification
{
return NovaNotification::make()
->icon('flag')
->message($this->getNotificationMessage())
->type(NovaNotification::INFO_TYPE)
->url($this->getNotificationUrl());
->url($this->getNovaNotificationUrl());
}
/**
@@ -63,7 +63,7 @@ abstract class WikiDeletedEvent extends BaseDeletedEvent implements NovaNotifica
*
* @return Collection
*/
public function getUsers(): Collection
public function getNovaNotificationRecipients(): Collection
{
return User::query()
->whereRelation(User::RELATION_ROLES, Role::ATTRIBUTE_NAME, 'Admin')
@@ -82,5 +82,5 @@ abstract class WikiDeletedEvent extends BaseDeletedEvent implements NovaNotifica
*
* @return string
*/
abstract protected function getNotificationUrl(): string;
abstract protected function getNovaNotificationUrl(): string;
}
+10
View File
@@ -57,6 +57,16 @@ abstract class BasePivotEvent implements DiscordMessageEvent
return Config::get(ServiceConstants::DB_UPDATES_DISCORD_CHANNEL_QUALIFIED);
}
/**
* Determine if the message should be sent.
*
* @return bool
*/
public function shouldSendDiscordMessage(): bool
{
return true;
}
/**
* Get the description for the Discord message payload.
*
+1 -1
View File
@@ -60,7 +60,7 @@ class PageDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = PageResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class AnimeDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = AnimeResource::uriKey();
@@ -73,7 +73,7 @@ class SynonymDeleted extends WikiDeletedEvent implements UpdateRelatedIndicesEve
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = SynonymResource::uriKey();
@@ -62,7 +62,7 @@ class EntryDeleted extends WikiDeletedEvent implements UpdateRelatedIndicesEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = EntryResource::uriKey();
+1 -1
View File
@@ -69,7 +69,7 @@ class ThemeDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = ThemeResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class ArtistDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = ArtistResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class AudioDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = AudioResource::uriKey();
@@ -60,7 +60,7 @@ class ExternalResourceDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = ExternalResourceResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class ImageDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = ImageResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class SeriesDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = SeriesResource::uriKey();
+1 -1
View File
@@ -65,7 +65,7 @@ class SongDeleted extends WikiDeletedEvent implements UpdateRelatedIndicesEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = SongResource::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class StudioDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = StudioResource::uriKey();
@@ -60,7 +60,7 @@ class VideoScriptDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = Script::uriKey();
+1 -1
View File
@@ -60,7 +60,7 @@ class VideoDeleted extends WikiDeletedEvent
*
* @return string
*/
protected function getNotificationUrl(): string
protected function getNovaNotificationUrl(): string
{
$uriKey = VideoResource::uriKey();
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
class AllowAudioStreams
{
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
class AllowDumpDownloading
{
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
class AllowPlaylistManagement
{
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
class AllowScriptDownloading
{
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Features;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Laravel\Pennant\Feature;
class AllowVideoStreams
{
public function resolve(?User $user): bool
{
if (! empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
return true;
}
return Feature::for(null)->value(static::class);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\Feature;
use App\Http\Api\Field\StringField;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\Feature;
/**
* Class FeatureNameField.
*/
class FeatureNameField extends StringField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Feature::ATTRIBUTE_NAME);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Admin\Feature;
use App\Contracts\Http\Api\Field\UpdatableField;
use App\Http\Api\Field\StringField;
use App\Http\Api\Schema\Schema;
use App\Models\Admin\Feature;
use Illuminate\Http\Request;
/**
* Class FeatureValueField.
*/
class FeatureValueField extends StringField implements UpdatableField
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, Feature::ATTRIBUTE_VALUE);
}
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array
{
return [
'sometimes',
'required',
'string',
'max:192',
];
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowAudioStreamsField.
*/
class FlagsAllowAudioStreamsField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_AUDIO_STREAMS_FLAG);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowDiscordNotifications.
*/
class FlagsAllowDiscordNotificationsField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowDumpDownloadingField.
*/
class FlagsAllowDumpDownloadingField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowPlaylistManagementField.
*/
class FlagsAllowPlaylistManagementField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_PLAYLIST_MANAGEMENT);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowScriptDownloadingField.
*/
class FlagsAllowScriptDownloadingField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowVideoStreamsField.
*/
class FlagsAllowVideoStreamsField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_VIDEO_STREAMS_FLAG);
}
}
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Field\Config\Flags;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Field\Field;
use App\Http\Api\Schema\Schema;
/**
* Class FlagsAllowViewRecordingField.
*/
class FlagsAllowViewRecordingField extends Field
{
/**
* Create a new field instance.
*
* @param Schema $schema
*/
public function __construct(Schema $schema)
{
parent::__construct($schema, FlagConstants::ALLOW_VIEW_RECORDING_FLAG);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\Admin;
use App\Http\Api\Field\Admin\Feature\FeatureNameField;
use App\Http\Api\Field\Admin\Feature\FeatureValueField;
use App\Http\Api\Field\Base\CreatedAtField;
use App\Http\Api\Field\Base\IdField;
use App\Http\Api\Field\Base\UpdatedAtField;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\EloquentSchema;
use App\Http\Resources\Admin\Resource\FeatureResource;
use App\Models\Admin\Feature;
/**
* Class FeatureSchema.
*/
class FeatureSchema extends EloquentSchema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return FeatureResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*/
public function fields(): array
{
return [
new IdField($this, Feature::ATTRIBUTE_ID),
new CreatedAtField($this),
new UpdatedAtField($this),
new FeatureNameField($this),
new FeatureValueField($this),
];
}
}
@@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Api\Schema\Config;
use App\Http\Api\Field\Config\Flags\FlagsAllowAudioStreamsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowDiscordNotificationsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowDumpDownloadingField;
use App\Http\Api\Field\Config\Flags\FlagsAllowPlaylistManagementField;
use App\Http\Api\Field\Config\Flags\FlagsAllowScriptDownloadingField;
use App\Http\Api\Field\Config\Flags\FlagsAllowVideoStreamsField;
use App\Http\Api\Field\Config\Flags\FlagsAllowViewRecordingField;
use App\Http\Api\Field\Field;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\Config\Resource\FlagsResource;
/**
* Class FlagsSchema.
*/
class FlagsSchema extends Schema
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string
{
return FlagsResource::$wrap;
}
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array
{
return [];
}
/**
* Get the direct fields of the resource.
*
* @return Field[]
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function fields(): array
{
return [
new FlagsAllowVideoStreamsField($this),
new FlagsAllowAudioStreamsField($this),
new FlagsAllowDiscordNotificationsField($this),
new FlagsAllowViewRecordingField($this),
new FlagsAllowDumpDownloadingField($this),
new FlagsAllowScriptDownloadingField($this),
new FlagsAllowPlaylistManagementField($this),
];
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\UpdateAction;
use App\Constants\FeatureConstants;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\UpdateRequest;
use App\Http\Resources\Admin\Collection\FeatureCollection;
use App\Http\Resources\Admin\Resource\FeatureResource;
use App\Models\Admin\Feature;
/**
* Class FeatureController.
*/
class FeatureController extends BaseController
{
/**
* Create a new controller instance.
*/
public function __construct()
{
parent::__construct(Feature::class, 'feature');
}
/**
* Display a listing of the resource.
*
* @param IndexRequest $request
* @param IndexAction $action
* @return FeatureCollection
*/
public function index(IndexRequest $request, IndexAction $action): FeatureCollection
{
$query = new Query($request->validated());
$builder = Feature::query()->where(Feature::ATTRIBUTE_SCOPE, FeatureConstants::NULL_SCOPE);
$features = $action->index($builder, $query, $request->schema());
return new FeatureCollection($features, $query);
}
/**
* Display the specified resource.
*
* @param ShowRequest $request
* @param Feature $feature
* @param ShowAction $action
* @return FeatureResource
*/
public function show(ShowRequest $request, Feature $feature, ShowAction $action): FeatureResource
{
$query = new Query($request->validated());
$show = $action->show($feature, $query, $request->schema());
return new FeatureResource($show, $query);
}
/**
* Update the specified resource.
*
* @param UpdateRequest $request
* @param Feature $feature
* @param UpdateAction $action
* @return FeatureResource
*/
public function update(UpdateRequest $request, Feature $feature, UpdateAction $action): FeatureResource
{
$updated = $action->update($feature, $request->validated());
return new FeatureResource($updated, new Query());
}
}
@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Config;
use App\Contracts\Http\Api\InteractsWithSchema;
use App\Http\Api\Query\Query;
use App\Http\Api\Schema\Config\FlagsSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Resources\Config\Resource\FlagsResource;
/**
* Class FlagsController.
*/
class FlagsController extends Controller implements InteractsWithSchema
{
/**
* Flags resource.
*
* @param ShowRequest $request
* @return FlagsResource
*/
public function show(ShowRequest $request): FlagsResource
{
$query = new Query($request->validated());
return new FlagsResource($query);
}
/**
* Get the underlying schema.
*
* @return Schema
*/
public function schema(): Schema
{
return new FlagsSchema();
}
}
@@ -11,7 +11,7 @@ use App\Actions\Http\Api\List\Playlist\Track\RestoreTrackAction;
use App\Actions\Http\Api\List\Playlist\Track\StoreTrackAction;
use App\Actions\Http\Api\List\Playlist\Track\UpdateTrackAction;
use App\Actions\Http\Api\ShowAction;
use App\Constants\Config\FlagConstants;
use App\Features\AllowPlaylistManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Models\List\PlaylistExceedsTrackLimit;
@@ -26,6 +26,7 @@ use App\Models\List\Playlist\PlaylistTrack;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class TrackController.
@@ -39,9 +40,9 @@ class TrackController extends BaseController
{
parent::__construct(PlaylistTrack::class, 'track,playlist');
$isPlaylistManagementAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED)
->append(',Playlist Management Disabled')
$isPlaylistManagementAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowPlaylistManagement::class)
->__toString();
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
@@ -11,8 +11,8 @@ use App\Actions\Http\Api\RestoreAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Actions\Http\Api\UpdateAction;
use App\Constants\Config\FlagConstants;
use App\Enums\Models\List\PlaylistVisibility;
use App\Features\AllowPlaylistManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Models\List\UserExceedsPlaylistLimit;
@@ -26,6 +26,7 @@ use App\Models\List\Playlist;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class PlaylistController.
@@ -39,9 +40,9 @@ class PlaylistController extends BaseController
{
parent::__construct(Playlist::class, 'playlist');
$isPlaylistManagementAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED)
->append(',Playlist Management Disabled')
$isPlaylistManagementAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowPlaylistManagement::class)
->__toString();
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
@@ -8,8 +8,8 @@ use App\Actions\Http\Api\DestroyAction;
use App\Actions\Http\Api\IndexAction;
use App\Actions\Http\Api\ShowAction;
use App\Actions\Http\Api\StoreAction;
use App\Constants\Config\FlagConstants;
use App\Enums\Models\List\PlaylistVisibility;
use App\Features\AllowPlaylistManagement;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\Pivot\PivotController;
use App\Http\Requests\Api\IndexRequest;
@@ -23,6 +23,7 @@ use App\Pivots\List\PlaylistImage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class PlaylistImageController.
@@ -36,9 +37,9 @@ class PlaylistImageController extends PivotController
{
parent::__construct(Playlist::class, 'playlist', Image::class, 'image');
$isPlaylistManagementAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED)
->append(',Playlist Management Disabled')
$isPlaylistManagementAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowPlaylistManagement::class)
->__toString();
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
-2
View File
@@ -7,7 +7,6 @@ namespace App\Http;
use App\Http\Middleware\Api\SetAcceptJsonHeader;
use App\Http\Middleware\Auth\Authenticate;
use App\Http\Middleware\Auth\RedirectIfAuthenticated;
use App\Http\Middleware\IsFeatureEnabled;
use App\Http\Middleware\LogRequest;
use App\Http\Middleware\Models\RecordView;
use App\Http\Middleware\ThrottleRequestsWithService;
@@ -97,7 +96,6 @@ class Kernel extends HttpKernel
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'is_feature_enabled' => IsFeatureEnabled::class,
'password.confirm' => RequirePassword::class,
'record_view' => RecordView::class,
'signed' => ValidateSignature::class,
-43
View File
@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class IsFeatureEnabled.
*/
class IsFeatureEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @param string $flag
* @param string $message
* @return mixed
*
* @throws HttpException
* @throws NotFoundHttpException
*/
public function handle(Request $request, Closure $next, string $flag, string $message): mixed
{
/** @var User|null $user */
$user = $request->user('sanctum');
if (! Config::bool($flag) && empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))) {
abort(403, $message);
}
return $next($request);
}
}
+3 -3
View File
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Http\Middleware\Models;
use App\Constants\Config\FlagConstants;
use App\Constants\FeatureConstants;
use Closure;
use CyrildeWit\EloquentViewable\Contracts\Viewable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
use RuntimeException;
/**
@@ -32,7 +32,7 @@ class RecordView
throw new RuntimeException('record_view should only be configured for viewable models');
}
if (Config::bool(FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED)) {
if (Feature::active(FeatureConstants::ALLOW_VIEW_RECORDING)) {
views($model)->cooldown(now()->addMinutes(5))->record();
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Admin\Collection;
use App\Http\Resources\Admin\Resource\FeatureResource;
use App\Http\Resources\BaseCollection;
use App\Models\Admin\Feature;
use Illuminate\Http\Request;
/**
* Class FeatureCollection.
*/
class FeatureCollection extends BaseCollection
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'features';
/**
* Transform the resource collection into an array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray(Request $request): array
{
return $this->collection->map(
fn (Feature $feature) => new FeatureResource($feature, $this->query)
)->all();
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Admin\Resource;
use App\Http\Api\Schema\Admin\FeatureSchema;
use App\Http\Api\Schema\Schema;
use App\Http\Resources\BaseResource;
/**
* Class FeatureResource.
*/
class FeatureResource extends BaseResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'feature';
/**
* Get the resource schema.
*
* @return Schema
*/
protected function schema(): Schema
{
return new FeatureSchema();
}
}
@@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Resources\Config\Resource;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Query\Query;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\MissingValue;
use Illuminate\Support\Facades\Config;
/**
* Class FlagsResource.
*/
class FlagsResource extends JsonResource
{
/**
* The "data" wrapper that should be applied.
*
* @var string|null
*/
public static $wrap = 'flags';
/**
* Create a new resource instance.
*
* @param Query $query
* @return void
*/
public function __construct(protected readonly Query $query)
{
parent::__construct(new MissingValue());
}
/**
* Transform the resource into an array.
*
* @param Request $request
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toArray($request): array
{
$result = [];
$criteria = $this->query->getFieldCriteria(static::$wrap);
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG)) {
$result[FlagConstants::ALLOW_VIDEO_STREAMS_FLAG] = Config::bool(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG)) {
$result[FlagConstants::ALLOW_AUDIO_STREAMS_FLAG] = Config::bool(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG)) {
$result[FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG] = Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_VIEW_RECORDING_FLAG)) {
$result[FlagConstants::ALLOW_VIEW_RECORDING_FLAG] = Config::bool(FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG)) {
$result[FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG] = Config::bool(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG)) {
$result[FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG] = Config::bool(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED);
}
if ($criteria === null || $criteria->isAllowedField(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT)) {
$result[FlagConstants::ALLOW_PLAYLIST_MANAGEMENT] = Config::bool(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED);
}
return $result;
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Jobs;
use App\Constants\Config\FlagConstants;
use App\Constants\FeatureConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Jobs\Middleware\RateLimited;
use App\Notifications\DiscordNotification;
@@ -14,8 +14,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Notification;
use Laravel\Pennant\Feature;
/**
* Class SendDiscordNotification.
@@ -44,7 +44,7 @@ class SendDiscordNotificationJob implements ShouldQueue
*/
public function handle(): void
{
if (Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED)) {
if (Feature::active(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS)) {
Notification::route('discord', $this->event->getDiscordChannel())
->notify(new DiscordNotification($this->event->getDiscordMessage()));
}
+3 -3
View File
@@ -23,9 +23,9 @@ class NotifiesNovaUsers implements ShouldQueue
*/
public function handle(NovaNotificationEvent $event): void
{
if ($event->shouldSend()) {
$notification = $event->getNotification();
foreach ($event->getUsers() as $user) {
if ($event->shouldSendNovaNotification()) {
$notification = $event->getNovaNotification();
foreach ($event->getNovaNotificationRecipients() as $user) {
$user->notify($notification);
}
}
+14 -3
View File
@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace App\Listeners;
use App\Constants\Config\FlagConstants;
use App\Constants\FeatureConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Jobs\SendDiscordNotificationJob;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
/**
* Class SendDiscordNotification.
@@ -26,8 +26,19 @@ class SendDiscordNotification implements ShouldQueue
*/
public function handle(DiscordMessageEvent $event): void
{
if (Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED)) {
if (Feature::active(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS)) {
SendDiscordNotificationJob::dispatch($event);
}
}
/**
* Determine whether the listener should be queued.
*
* @param DiscordMessageEvent $event
* @return bool
*/
public function shouldQueue(DiscordMessageEvent $event): bool
{
return $event->shouldSendDiscordMessage();
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Models\Admin;
use App\Constants\FeatureConstants;
use App\Events\Admin\Feature\FeatureCreated;
use App\Events\Admin\Feature\FeatureDeleted;
use App\Events\Admin\Feature\FeatureUpdated;
use Database\Factories\Admin\FeatureFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Laravel\Nova\Actions\Actionable;
/**
* Class Feature.
*
* @property Carbon $created_at
* @property int $feature_id
* @property string $name
* @property string $scope
* @property Carbon $updated_at
* @property string $value
*
* @method static FeatureFactory factory(...$parameters)
*/
class Feature extends Model
{
use Actionable;
use HasFactory;
final public const TABLE = 'features';
final public const ATTRIBUTE_ID = 'feature_id';
final public const ATTRIBUTE_NAME = 'name';
final public const ATTRIBUTE_SCOPE = 'scope';
final public const ATTRIBUTE_VALUE = 'value';
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
Feature::ATTRIBUTE_VALUE,
];
/**
* The event map for the model.
*
* Allows for object-based events for native Eloquent events.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => FeatureCreated::class,
'deleted' => FeatureDeleted::class,
'updated' => FeatureUpdated::class,
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = Feature::TABLE;
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = Feature::ATTRIBUTE_ID;
/**
* Get name.
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Determine if the feature scope is global.
*
* @return bool
*/
public function isNullScope(): bool
{
return $this->scope === FeatureConstants::NULL_SCOPE;
}
}
+3 -3
View File
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Notifications;
use App\Constants\Config\FlagConstants;
use App\Constants\FeatureConstants;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
use NotificationChannels\Discord\DiscordChannel;
use NotificationChannels\Discord\DiscordMessage;
@@ -66,6 +66,6 @@ class DiscordNotification extends Notification implements ShouldQueue
*/
public function shouldSend(mixed $notifiable, string $channel): bool
{
return Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED);
return Feature::active(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Nova\Resources\Admin;
use App\Models\Admin\Feature as FeatureModel;
use App\Nova\Resources\BaseResource;
use Exception;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Query\Search\Column;
/**
* Class Feature.
*/
class Feature extends BaseResource
{
/**
* The model the resource corresponds to.
*
* @var string
*/
public static string $model = FeatureModel::class;
/**
* The single value that should be used to represent the resource when being displayed.
*
* @var string
*/
public static $title = FeatureModel::ATTRIBUTE_NAME;
/**
* The logical group associated with the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function group(): string
{
return __('nova.resources.group.admin');
}
/**
* Get the displayable label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function label(): string
{
return __('nova.resources.label.features');
}
/**
* Get the displayable singular label of the resource.
*
* @return string
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function singularLabel(): string
{
return __('nova.resources.singularLabel.feature');
}
/**
* Get the searchable columns for the resource.
*
* @return array
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function searchableColumns(): array
{
return [
new Column(FeatureModel::ATTRIBUTE_NAME),
];
}
/**
* Indicates if the resource should be globally searchable.
*
* @var bool
*/
public static $globallySearchable = false;
/**
* Determine if this resource uses Laravel Scout.
*
* @return bool
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public static function usesScout(): bool
{
return false;
}
/**
* Get the fields displayed by the resource.
*
* @param NovaRequest $request
* @return array
*
* @throws Exception
*/
public function fields(NovaRequest $request): array
{
return [
ID::make(__('nova.fields.base.id'), FeatureModel::ATTRIBUTE_ID)
->sortable()
->showOnPreview()
->showWhenPeeking(),
Text::make(__('nova.fields.feature.key.name'), FeatureModel::ATTRIBUTE_NAME)
->sortable()
->copyable()
->readonly()
->help(__('nova.fields.feature.key.help'))
->showOnPreview()
->filterable()
->showWhenPeeking(),
Text::make(__('nova.fields.feature.value.name'), FeatureModel::ATTRIBUTE_VALUE)
->sortable()
->copyable()
->rules(['required', 'max:192'])
->help(__('nova.fields.feature.value.help'))
->showOnPreview()
->filterable()
->maxlength(192)
->enforceMaxlength()
->showWhenPeeking(),
];
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Policies\Admin;
use App\Enums\Auth\CrudPermission;
use App\Models\Admin\Feature;
use App\Models\Auth\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Laravel\Nova\Nova;
/**
* Class FeaturePolicy.
*/
class FeaturePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param User|null $user
* @return bool
*/
public function viewAny(?User $user): bool
{
return Nova::whenServing(
fn (): bool => $user !== null && $user->can(CrudPermission::VIEW()->format(Feature::class)),
fn (): bool => true
);
}
/**
* Determine whether the user can view the model.
*
* @param User|null $user
* @param Feature $feature
* @return bool
*/
public function view(?User $user, Feature $feature): bool
{
return Nova::whenServing(
fn (): bool => $user !== null && $user->can(CrudPermission::VIEW()->format(Feature::class)),
fn (): bool => $feature->isNullScope()
);
}
/**
* Determine whether the user can create models.
*
* @param User $user
* @return bool
*/
public function create(User $user): bool
{
return $user->can(CrudPermission::CREATE()->format(Feature::class));
}
/**
* Determine whether the user can update the model.
*
* @param User $user
* @return bool
*/
public function update(User $user): bool
{
return $user->can(CrudPermission::UPDATE()->format(Feature::class));
}
/**
* Determine whether the user can delete the model.
*
* @param User $user
* @return bool
*/
public function delete(User $user): bool
{
return $user->can(CrudPermission::DELETE()->format(Feature::class));
}
}
+6 -64
View File
@@ -4,18 +4,12 @@ declare(strict_types=1);
namespace App\Providers;
use App\Constants\Config\AudioConstants;
use App\Constants\Config\DumpConstants;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use Illuminate\Config\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\Facades\Config;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
/**
* Class AppServiceProvider.
@@ -29,17 +23,12 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Repository::macro('bool', function (string $key, bool $default = false): bool {
/** @var Repository $this */
return filter_var($this->get($key, $default), FILTER_VALIDATE_BOOLEAN);
});
Model::preventLazyLoading();
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = get_class($model);
Log::error("Attempted to lazy load [$relation] on model [$class]");
Log::error("Attempted to lazy load '$relation' on model '$class'");
});
Model::preventsAccessingMissingAttributes();
@@ -47,56 +36,9 @@ class AppServiceProvider extends ServiceProvider
Model::handleMissingAttributeViolationUsing(function (Model $model, string $key) {
$class = get_class($model);
Log::error("Attribute $key does not exist or was not retrieved for model [$class]");
Log::error("Attribute '$key' does not exist or was not retrieved for model '$class'");
});
AboutCommand::add('Audios', [
'Default Disk' => fn () => Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED),
'Disks' => fn () => implode(',', Config::get(AudioConstants::DISKS_QUALIFIED)),
'Nginx Redirect' => fn () => Config::get(AudioConstants::NGINX_REDIRECT_QUALIFIED),
'Streaming Method' => fn () => Config::get(AudioConstants::STREAMING_METHOD_QUALIFIED),
]);
AboutCommand::add('Dumps', [
'Disk' => fn () => Config::get(DumpConstants::DISK_QUALIFIED),
]);
AboutCommand::add('Flags', [
'Allow Audio Streams' => fn () => Config::bool(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Discord Notifications' => fn () => Config::bool(FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Dump Downloading' => fn () => Config::bool(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Playlist Management' => fn () => Config::bool(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED) ? 'true' : 'false',
'Allow Script Downloading' => fn () => Config::bool(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow Video Streams' => fn () => Config::bool(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED) ? 'true' : 'false',
'Allow View Recording' => fn () => Config::bool(FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED) ? 'true' : 'false',
]);
AboutCommand::add('Images', [
'Disk' => fn () => Config::get('image.disk'),
]);
AboutCommand::add('Videos', [
'Default Disk' => fn () => Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED),
'Disks' => fn () => implode(',', Config::get(VideoConstants::DISKS_QUALIFIED)),
'Encoder Version' => fn () => Config::get(VideoConstants::ENCODER_VERSION_QUALIFIED),
'Nginx Redirect' => fn () => Config::get(VideoConstants::NGINX_REDIRECT_QUALIFIED),
'Script Disk' => fn () => Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED),
'Streaming Method' => fn () => Config::get(VideoConstants::STREAMING_METHOD_QUALIFIED),
]);
AboutCommand::add('Wiki', [
'Featured Entry' => function () {
/** @var AnimeThemeEntry|null $entry */
$entry = AnimeThemeEntry::query()->find(Config::get('wiki.featured_entry'));
return $entry?->getName();
},
'Featured Video' => function () {
/** @var Video|null $video */
$video = Video::query()->find(Config::get('wiki.featured_video'));
return $video?->getName();
},
]);
EnsureFeaturesAreActive::whenInactive(fn (Request $request, array $features) => new Response(status: 403));
}
}
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Rules\Wiki\Submission\Format;
use App\Constants\Config\VideoConstants;
use App\Constants\FeatureConstants;
use App\Rules\Wiki\Submission\SubmissionRule;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Laravel\Pennant\Feature;
/**
* Class EncoderVersionFormatRule.
@@ -28,7 +28,7 @@ class EncoderVersionFormatRule extends SubmissionRule
$encoder = Arr::get($tags, 'encoder');
return version_compare($encoder, Config::get(VideoConstants::ENCODER_VERSION_QUALIFIED), '>=');
return version_compare($encoder, Feature::value(FeatureConstants::REQUIRED_ENCODER_VERSION), '>=');
}
/**
+1
View File
@@ -41,6 +41,7 @@
"laravel/framework": "^10.7",
"laravel/horizon": "^5.12",
"laravel/nova": "^4.22.2",
"laravel/pennant": "^1.2",
"laravel/sanctum": "^3.2",
"laravel/scout": "^10.0",
"laravel/telescope": "^4.12",
Generated
+77 -1
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": "ab2b43c9ac80ff850075f012223346d9",
"content-hash": "9c9bd83c569aefbc7ee1cbfe7186a0d1",
"packages": [
{
"name": "akaunting/laravel-setting",
@@ -3502,6 +3502,82 @@
},
"time": "2023-04-07T18:14:13+00:00"
},
{
"name": "laravel/pennant",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pennant.git",
"reference": "783ea43eaa51867247192b30505af0ef5f19ccf1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pennant/zipball/783ea43eaa51867247192b30505af0ef5f19ccf1",
"reference": "783ea43eaa51867247192b30505af0ef5f19ccf1",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0",
"illuminate/container": "^10.0",
"illuminate/contracts": "^10.0",
"illuminate/database": "^10.0",
"illuminate/queue": "^10.0",
"illuminate/support": "^10.0",
"php": "^8.1",
"symfony/finder": "^6.0"
},
"require-dev": {
"laravel/octane": "^1.4",
"orchestra/testbench": "^8.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Pennant\\PennantServiceProvider"
],
"aliases": {
"Feature": "Laravel\\Pennant\\Feature"
}
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Laravel\\Pennant\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "A simple, lightweight library for managing feature flags.",
"homepage": "https://github.com/laravel/pennant",
"keywords": [
"feature",
"flags",
"laravel",
"pennant"
],
"support": {
"issues": "https://github.com/laravel/pennant/issues",
"source": "https://github.com/laravel/pennant"
},
"time": "2023-03-17T18:08:19+00:00"
},
{
"name": "laravel/sanctum",
"version": "v3.2.1",
-98
View File
@@ -1,98 +0,0 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Allow Video Streams
|--------------------------------------------------------------------------
|
| When video streams are allowed, requests to the video.show route will
| stream video. If disabled, requests to the video.show route will
| raise a 403 Forbidden response.
|
*/
'allow_video_streams' => (bool) env('ALLOW_VIDEO_STREAMS', false),
/*
|--------------------------------------------------------------------------
| Allow Audio Streams
|--------------------------------------------------------------------------
|
| When audio streams are allowed, requests to the audio.show route will
| stream audio. If disabled, requests to the audio.show route will
| raise a 403 Forbidden response.
|
*/
'allow_audio_streams' => (bool) env('ALLOW_AUDIO_STREAMS', false),
/*
|--------------------------------------------------------------------------
| Allow Discord Notifications
|--------------------------------------------------------------------------
|
| When discord notifications are allowed, event listeners shall send discord
| notifications to the configured discord channel through the configured bot.
| If discord notifications are not allowed, event listeners shall not send
| discord notifications.
|
*/
'allow_discord_notifications' => (bool) env('ALLOW_DISCORD_NOTIFICATIONS', false),
/*
|--------------------------------------------------------------------------
| Allow View Recording
|--------------------------------------------------------------------------
|
| When set to true, a view will be created for the viewable model in the show
| action of the resource controller. When set to false, a view will not be
| recorded.
|
*/
'allow_view_recording' => (bool) env('ALLOW_VIEW_RECORDING', false),
/*
|--------------------------------------------------------------------------
| Allow Dump Downloading
|--------------------------------------------------------------------------
|
| When dump downloads are allowed, requests to the dump.show route will
| download dumps. If disabled, requests to the dump.show route will
| raise a 403 Forbidden response.
|
*/
'allow_dump_downloading' => (bool) env('ALLOW_DUMP_DOWNLOADING', false),
/*
|--------------------------------------------------------------------------
| Allow Script Downloading
|--------------------------------------------------------------------------
|
| When script downloads are allowed, requests to the videoscript.show route will
| download scripts. If disabled, requests to the videoscript.show route will
| raise a 403 Forbidden response.
|
*/
'allow_script_downloading' => (bool) env('ALLOW_SCRIPT_DOWNLOADING', false),
/*
|--------------------------------------------------------------------------
| Allow Playlist Management
|--------------------------------------------------------------------------
|
| When playlist management is allowed, requests to write routes for playlist resources
| permit authorized users. If disabled, requests to write routes for playlist resources
| will raise a 403 Forbidden response.
|
*/
'allow_playlist_management' => (bool) env('ALLOW_PLAYLIST_MANAGEMENT', false),
];
+44
View File
@@ -0,0 +1,44 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Pennant Store
|--------------------------------------------------------------------------
|
| Here you will specify the default store that Pennant should use when
| storing and resolving feature flag values. Pennant ships with the
| ability to store flag values in an in-memory array or database.
|
| Supported: "array", "database"
|
*/
'default' => env('PENNANT_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Pennant Stores
|--------------------------------------------------------------------------
|
| Here you may configure each of the stores that should be available to
| Pennant. These stores shall be used to store resolved feature flag
| values - you may configure as many as your application requires.
|
*/
'stores' => [
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'features',
],
],
];
-9
View File
@@ -2,8 +2,6 @@
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
use App\Constants\Config\WikiConstants;
use App\Models\Admin\Setting;
@@ -87,13 +85,6 @@ return [
|
*/
'override' => [
FlagConstants::ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED => FlagConstants::ALLOW_AUDIO_STREAMS_FLAG,
FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED => FlagConstants::ALLOW_VIDEO_STREAMS_FLAG,
FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED => FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG,
FlagConstants::ALLOW_VIEW_RECORDING_FLAG_QUALIFIED => FlagConstants::ALLOW_VIEW_RECORDING_FLAG,
FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED => FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG,
FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
VideoConstants::ENCODER_VERSION_QUALIFIED => VideoConstants::ENCODER_VERSION,
WikiConstants::FEATURED_ENTRY_SETTING_QUALIFIED => WikiConstants::FEATURED_ENTRY_SETTING,
WikiConstants::FEATURED_VIDEO_SETTING_QUALIFIED => WikiConstants::FEATURED_VIDEO_SETTING,
],
-13
View File
@@ -54,19 +54,6 @@ return [
'nginx_redirect' => env('VIDEO_NGINX_REDIRECT', '/video_redirect/'),
/*
|--------------------------------------------------------------------------
| Video Uploading
|--------------------------------------------------------------------------
|
| These values facilitate the validation and uploading of video in-system to object storage.
| For validation, we want to enforce that the latest FFmpeg version is used.
| We will use the libavformat version of the form "Lavf{major}.{minor}.{patch]".
|
*/
'encoder_version' => env('VIDEO_ENCODER_VERSION'),
/*
|--------------------------------------------------------------------------
| Video Scripts
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Database\Factories\Admin;
use App\Constants\FeatureConstants;
use App\Models\Admin\Feature;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* Class FeatureFactory.
*
* @method Feature createOne($attributes = [])
* @method Feature makeOne($attributes = [])
*
* @extends Factory<Feature>
*/
class FeatureFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<Feature>
*/
protected $model = Feature::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition(): array
{
return [
Feature::ATTRIBUTE_NAME => fake()->unique()->word(),
Feature::ATTRIBUTE_SCOPE => FeatureConstants::NULL_SCOPE,
Feature::ATTRIBUTE_VALUE => fake()->boolean(),
];
}
}
@@ -0,0 +1,39 @@
<?php
use App\Models\Admin\Feature;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (! Schema::hasTable(Feature::TABLE)) {
Schema::create(Feature::TABLE, function (Blueprint $table) {
$table->id(Feature::ATTRIBUTE_ID);
$table->string(Feature::ATTRIBUTE_NAME);
$table->string(Feature::ATTRIBUTE_SCOPE);
$table->text(Feature::ATTRIBUTE_VALUE);
$table->timestamps(6);
$table->unique([Feature::ATTRIBUTE_NAME, Feature::ATTRIBUTE_SCOPE]);
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists(Feature::TABLE);
}
};
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Database\Seeders\Admin\Feature;
use App\Constants\FeatureConstants;
use App\Features\AllowAudioStreams;
use App\Features\AllowDumpDownloading;
use App\Features\AllowPlaylistManagement;
use App\Features\AllowScriptDownloading;
use App\Features\AllowVideoStreams;
use Illuminate\Database\Seeder;
use Laravel\Pennant\Feature;
/**
* Class FeatureSeeder.
*/
class FeatureSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Feature::deactivate(AllowAudioStreams::class);
Feature::deactivate(AllowDumpDownloading::class);
Feature::deactivate(AllowPlaylistManagement::class);
Feature::deactivate(AllowScriptDownloading::class);
Feature::deactivate(AllowVideoStreams::class);
Feature::deactivate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
Feature::deactivate(FeatureConstants::ALLOW_VIEW_RECORDING);
Feature::deactivate(FeatureConstants::REQUIRED_ENCODER_VERSION);
}
}
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Database\Seeders\Admin\Setting;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\VideoConstants;
use App\Constants\Config\WikiConstants;
use App\Models\Admin\Setting;
use Illuminate\Database\Seeder;
@@ -23,76 +21,6 @@ class SettingSeeder extends Seeder
*/
public function run(): void
{
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_AUDIO_STREAMS_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_AUDIO_STREAMS_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_VIDEO_STREAMS_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_VIDEO_STREAMS_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_DISCORD_NOTIFICATIONS_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_VIEW_RECORDING_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_VIEW_RECORDING_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => VideoConstants::ENCODER_VERSION,
],
[
Setting::ATTRIBUTE_KEY => VideoConstants::ENCODER_VERSION,
Setting::ATTRIBUTE_VALUE => Config::get(VideoConstants::ENCODER_VERSION_QUALIFIED, ''),
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => WikiConstants::FEATURED_ENTRY_SETTING,
@@ -112,15 +40,5 @@ class SettingSeeder extends Seeder
Setting::ATTRIBUTE_VALUE => Config::get(WikiConstants::FEATURED_VIDEO_SETTING_QUALIFIED, ''),
]
);
Setting::query()->firstOrCreate(
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_PLAYLIST_MANAGEMENT,
],
[
Setting::ATTRIBUTE_KEY => FlagConstants::ALLOW_PLAYLIST_MANAGEMENT,
Setting::ATTRIBUTE_VALUE => 'false',
]
);
}
}
@@ -9,6 +9,7 @@ use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Auth\SpecialPermission;
use App\Models\Admin\Announcement;
use App\Models\Admin\Dump;
use App\Models\Admin\Feature;
use App\Models\Admin\Setting;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
@@ -49,6 +50,7 @@ class PermissionSeeder extends Seeder
$this->registerResource(Announcement::class, ExtendedCrudPermission::getInstances());
$this->registerResource(Dump::class, ExtendedCrudPermission::getInstances());
$this->registerResource(Setting::class, CrudPermission::getInstances());
$this->registerResource(Feature::class, CrudPermission::getInstances());
// Auth Resources
$this->registerResource(Permission::class, [CrudPermission::VIEW()]);
@@ -9,6 +9,7 @@ use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Auth\SpecialPermission;
use App\Models\Admin\Announcement;
use App\Models\Admin\Dump;
use App\Models\Admin\Feature;
use App\Models\Admin\Setting;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
@@ -50,6 +51,7 @@ class AdminSeeder extends RoleSeeder
// Admin Resources
$this->configureResource($role, Announcement::class, ExtendedCrudPermission::getInstances());
$this->configureResource($role, Dump::class, ExtendedCrudPermission::getInstances());
$this->configureResource($role, Feature::class, [CrudPermission::VIEW(), CrudPermission::UPDATE()]);
$this->configureResource($role, Setting::class, CrudPermission::getInstances());
// Auth Resources
+2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Database\Seeders;
use Database\Seeders\Admin\Feature\FeatureSeeder;
use Database\Seeders\Auth\Permission\PermissionSeeder;
use Database\Seeders\Auth\Role\RoleSeeder;
use Database\Seeders\Billing\Transaction\DigitalOceanTransactionSeeder;
@@ -29,5 +30,6 @@ class DatabaseSeeder extends Seeder
$this->call(VideoSeeder::class);
$this->call(AudioSeeder::class);
$this->call(HashidsSeeder::class);
$this->call(FeatureSeeder::class);
}
}
+12
View File
@@ -492,6 +492,16 @@ return [
'name' => 'Site',
],
],
'feature' => [
'key' => [
'help' => 'The name of the feature. Class strings label features that require business logic to resolve. Otherwise, this is global value.',
'name' => 'Key',
],
'value' => [
'help' => 'Binary state features should be true or false. Rich Feature values can be any string.',
'name' => 'Value',
],
],
'image' => [
'facet' => [
'help' => 'The page component that the image is intended for. Example: Is this a small cover image or a large cover image?',
@@ -801,6 +811,7 @@ return [
'balances' => 'Balances',
'dumps' => 'Dumps',
'external_resources' => 'External Resources',
'features' => 'Features',
'groups' => 'Groups',
'images' => 'Images',
'members' => 'Members',
@@ -829,6 +840,7 @@ return [
'balance' => 'Balance',
'dump' => 'Dump',
'external_resource' => 'External Resource',
'feature' => 'Feature',
'image' => 'Image',
'page' => 'Page',
'permission' => 'Permission',
+3 -2
View File
@@ -4,12 +4,12 @@ declare(strict_types=1);
use App\Http\Controllers\Api\Admin\AnnouncementController;
use App\Http\Controllers\Api\Admin\DumpController;
use App\Http\Controllers\Api\Admin\FeatureController;
use App\Http\Controllers\Api\Auth\User\Me\List\MyPlaylistController;
use App\Http\Controllers\Api\Auth\User\Me\MyController;
use App\Http\Controllers\Api\Billing\BalanceController;
use App\Http\Controllers\Api\Billing\TransactionController;
use App\Http\Controllers\Api\Billing\TransparencyController;
use App\Http\Controllers\Api\Config\FlagsController;
use App\Http\Controllers\Api\Config\WikiController;
use App\Http\Controllers\Api\Document\PageController;
use App\Http\Controllers\Api\List\Playlist\TrackBackwardController;
@@ -222,6 +222,8 @@ if (! function_exists('apiPivotResourceUri')) {
// Admin Routes
apiResource('announcement', AnnouncementController::class);
apiResource('dump', DumpController::class);
Route::apiResource('feature', FeatureController::class)
->only(['index', 'show', 'update']);
// Auth Routes
Route::get('/me', [MyController::class, 'show'])->name('me.show');
@@ -233,7 +235,6 @@ apiResource('transaction', TransactionController::class);
Route::get('transparency', [TransparencyController::class, 'show'])->name('transparency.show');
// Config Routes
Route::get('config/flags', [FlagsController::class, 'show'])->name('config.flags.show');
Route::get('config/wiki', [WikiController::class, 'show'])->name('config.wiki.show');
// Document Routes
+5 -4
View File
@@ -2,14 +2,15 @@
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Features\AllowAudioStreams;
use App\Http\Controllers\Wiki\Audio\AudioController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
$isAudioStreamingAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_AUDIO_STREAMS_FLAG_QUALIFIED)
->append(',Audio Streaming Disabled')
$isAudioStreamingAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowAudioStreams::class)
->__toString();
Route::get('/{audio}', [AudioController::class, 'show'])
+5 -4
View File
@@ -2,16 +2,17 @@
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Features\AllowDumpDownloading;
use App\Http\Controllers\Admin\DumpController;
use App\Http\Controllers\Admin\LatestDocumentDumpController;
use App\Http\Controllers\Admin\LatestWikiDumpController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
$isDumpDownloadingAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED)
->append(',Dump Downloading Disabled')
$isDumpDownloadingAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowDumpDownloading::class)
->__toString();
Route::get('/latest/document', [LatestDocumentDumpController::class, 'show'])
+5 -4
View File
@@ -2,14 +2,15 @@
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Features\AllowScriptDownloading;
use App\Http\Controllers\Wiki\Video\Script\ScriptController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
$isScriptDownloadingAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED)
->append(',Script Downloading Disabled')
$isScriptDownloadingAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowScriptDownloading::class)
->__toString();
Route::get('/{videoscript}', [ScriptController::class, 'show'])
+5 -4
View File
@@ -2,14 +2,15 @@
declare(strict_types=1);
use App\Constants\Config\FlagConstants;
use App\Features\AllowVideoStreams;
use App\Http\Controllers\Wiki\Video\VideoController;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
$isVideoStreamingAllowed = Str::of('is_feature_enabled:')
->append(FlagConstants::ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED)
->append(',Video Streaming Disabled')
$isVideoStreamingAllowed = Str::of(EnsureFeaturesAreActive::class)
->append(':')
->append(AllowVideoStreams::class)
->__toString();
Route::get('/{video}', [VideoController::class, 'show'])
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Events\Admin;
use App\Events\Admin\Feature\FeatureCreated;
use App\Events\Admin\Feature\FeatureDeleted;
use App\Events\Admin\Feature\FeatureUpdated;
use App\Models\Admin\Feature;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
/**
* Class FeatureTest.
*/
class FeatureTest extends TestCase
{
/**
* When a Feature is created, a FeatureCreated event shall be dispatched.
*
* @return void
*/
public function testFeatureCreatedEventDispatched(): void
{
Feature::factory()->create();
Event::assertDispatched(FeatureCreated::class);
}
/**
* When a Feature is deleted, a FeatureDeleted event shall be dispatched.
*
* @return void
*/
public function testFeatureDeletedEventDispatched(): void
{
$feature = Feature::factory()->create();
$feature->delete();
Event::assertDispatched(FeatureDeleted::class);
}
/**
* When a Feature is updated, a FeatureUpdated event shall be dispatched.
*
* @return void
*/
public function testFeatureUpdatedEventDispatched(): void
{
$feature = Feature::factory()->createOne();
$feature->update([
Feature::ATTRIBUTE_VALUE => ! $feature->value,
]);
Event::assertDispatched(FeatureUpdated::class);
}
/**
* The FeatureUpdated event shall contain embed fields.
*
* @return void
*/
public function testFeatureUpdatedEventEmbedFields(): void
{
$feature = Feature::factory()->createOne();
$feature->update([
Feature::ATTRIBUTE_VALUE => ! $feature->value,
]);
Event::assertDispatched(FeatureUpdated::class, function (FeatureUpdated $event) {
$message = $event->getDiscordMessage();
return ! empty(Arr::get($message->embed, 'fields'));
});
}
}
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more