mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
feat(api): add configurable limits to playlist and track creation (#545)
This commit is contained in:
@@ -210,6 +210,10 @@ NOVA_LICENSE_KEY=
|
||||
NOVA_PASSWORDS=null
|
||||
NOVA_PATH=/admin
|
||||
|
||||
# playlist
|
||||
PLAYLIST_MAX_TRACKS=1000
|
||||
USER_MAX_PLAYLISTS=1000
|
||||
|
||||
# queue
|
||||
QUEUE_CONNECTION=sync
|
||||
REDIS_QUEUE=default
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Constants\Config;
|
||||
|
||||
/**
|
||||
* Class PlaylistConstants.
|
||||
*/
|
||||
class PlaylistConstants
|
||||
{
|
||||
final public const MAX_PLAYLISTS_QUALIFIED = 'playlist.user_max_playlists';
|
||||
|
||||
final public const MAX_TRACKS_QUALIFIED = 'playlist.playlist_max_tracks';
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use App\Actions\Http\Api\ShowAction;
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Controllers\Api\BaseController;
|
||||
use App\Http\Middleware\Models\List\PlaylistExceedsTrackLimit;
|
||||
use App\Http\Requests\Api\IndexRequest;
|
||||
use App\Http\Requests\Api\ShowRequest;
|
||||
use App\Http\Requests\Api\StoreRequest;
|
||||
@@ -44,6 +45,7 @@ class TrackController extends BaseController
|
||||
->__toString();
|
||||
|
||||
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
|
||||
$this->middleware(PlaylistExceedsTrackLimit::class)->only(['store', 'restore']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Constants\Config\FlagConstants;
|
||||
use App\Enums\Models\List\PlaylistVisibility;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Controllers\Api\BaseController;
|
||||
use App\Http\Middleware\Models\List\UserExceedsPlaylistLimit;
|
||||
use App\Http\Requests\Api\IndexRequest;
|
||||
use App\Http\Requests\Api\ShowRequest;
|
||||
use App\Http\Requests\Api\StoreRequest;
|
||||
@@ -45,6 +46,7 @@ class PlaylistController extends BaseController
|
||||
->__toString();
|
||||
|
||||
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
|
||||
$this->middleware(UserExceedsPlaylistLimit::class)->only(['store', 'restore']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware\Models\List;
|
||||
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\List\Playlist;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class PlaylistExceedsTrackLimit
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure(Request): mixed $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
$trackLimit = intval(Config::get(PlaylistConstants::MAX_TRACKS_QUALIFIED));
|
||||
|
||||
/** @var Playlist|null $playlist */
|
||||
$playlist = $request->route('playlist');
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = $request->user('sanctum');
|
||||
|
||||
if (
|
||||
intval($playlist?->tracks()?->count()) >= $trackLimit
|
||||
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))
|
||||
) {
|
||||
abort(403, "Playlists cannot contain more than '$trackLimit' tracks.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware\Models\List;
|
||||
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Models\Auth\User;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class UserExceedsPlaylistLimit
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure(Request): mixed $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
$playlistLimit = intval(Config::get(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED));
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = $request->user('sanctum');
|
||||
|
||||
if (
|
||||
intval($user?->playlists()?->count()) >= $playlistLimit
|
||||
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))
|
||||
) {
|
||||
abort(403, "User cannot have more than '$playlistLimit' playlists.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Limits
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These values represent caps on playlists to prevent spam. By default,
|
||||
| an individual playlist is permitted 1000 tracks, and a user
|
||||
| is permitted 1000 playlists.
|
||||
|
|
||||
*/
|
||||
|
||||
'playlist_max_tracks' => (int) env('PLAYLIST_MAX_TRACKS', 1000),
|
||||
|
||||
'user_max_playlists' => (int) env('USER_MAX_PLAYLISTS', 1000),
|
||||
];
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Http\Api\List\Playlist;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\ExtendedCrudPermission;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Models\Auth\User;
|
||||
@@ -183,4 +184,68 @@ class PlaylistRestoreTest extends TestCase
|
||||
$response->assertOk();
|
||||
static::assertNotSoftDeleted($playlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Playlist Restore Endpoint shall forbid users from restoring playlists that exceed the user playlist limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimit(): void
|
||||
{
|
||||
$playlistLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()
|
||||
->has(Playlist::factory()->count($playlistLimit))
|
||||
->withPermissions(ExtendedCrudPermission::RESTORE()->format(Playlist::class))
|
||||
->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$playlist->delete();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->patch(route('api.playlist.restore', ['playlist' => $playlist]));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Playlist Restore Endpoint shall permit users with bypass feature flag permission
|
||||
* to restore playlists that exceed the user playlist limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimitPermittedForBypass(): void
|
||||
{
|
||||
$playlistLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()
|
||||
->has(Playlist::factory()->count($playlistLimit))
|
||||
->withPermissions(
|
||||
ExtendedCrudPermission::RESTORE()->format(Playlist::class),
|
||||
SpecialPermission::BYPASS_FEATURE_FLAGS
|
||||
)
|
||||
->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$playlist->delete();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->patch(route('api.playlist.restore', ['playlist' => $playlist]));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Http\Api\List\Playlist;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\CrudPermission;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Enums\Models\List\PlaylistVisibility;
|
||||
@@ -158,4 +159,66 @@ class PlaylistStoreTest extends TestCase
|
||||
|
||||
$response->assertCreated();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Playlist Store Endpoint shall forbid users from creating playlists that exceed the user playlist limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimit(): void
|
||||
{
|
||||
$playlistLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$parameters = array_merge(
|
||||
Playlist::factory()->raw(),
|
||||
[Playlist::ATTRIBUTE_VISIBILITY => PlaylistVisibility::getRandomInstance()->description],
|
||||
);
|
||||
|
||||
$user = User::factory()
|
||||
->has(Playlist::factory()->count($playlistLimit))
|
||||
->withPermissions(CrudPermission::CREATE()->format(Playlist::class))
|
||||
->createOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->post(route('api.playlist.store', $parameters));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Playlist Store Endpoint shall permit users with bypass feature flag permission
|
||||
* to create playlists that exceed the user playlist limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimitPermittedForBypass(): void
|
||||
{
|
||||
$playlistLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$parameters = array_merge(
|
||||
Playlist::factory()->raw(),
|
||||
[Playlist::ATTRIBUTE_VISIBILITY => PlaylistVisibility::getRandomInstance()->description],
|
||||
);
|
||||
|
||||
$user = User::factory()
|
||||
->has(Playlist::factory()->count($playlistLimit))
|
||||
->withPermissions(
|
||||
CrudPermission::CREATE()->format(Playlist::class),
|
||||
SpecialPermission::BYPASS_FEATURE_FLAGS
|
||||
)
|
||||
->createOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->post(route('api.playlist.store', $parameters));
|
||||
|
||||
$response->assertCreated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Http\Api\List\Playlist\Track;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\CrudPermission;
|
||||
use App\Enums\Auth\ExtendedCrudPermission;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
@@ -400,4 +401,80 @@ class TrackRestoreTest extends TestCase
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Track Restore Endpoint shall forbid users from restoring playlist tracks that exceed the max track limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimit(): void
|
||||
{
|
||||
$trackLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()
|
||||
->withPermissions(
|
||||
CrudPermission::DELETE()->format(PlaylistTrack::class),
|
||||
ExtendedCrudPermission::RESTORE()->format(PlaylistTrack::class)
|
||||
)
|
||||
->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->tracks($trackLimit)
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$track = PlaylistTrack::factory()
|
||||
->for($playlist)
|
||||
->createOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->delete(route('api.playlist.track.destroy', ['playlist' => $playlist, 'track' => $track]));
|
||||
|
||||
$response = $this->patch(route('api.playlist.track.restore', ['playlist' => $playlist, 'track' => $track]));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Track Restore Endpoint shall permit users with bypass feature flag permission
|
||||
* to restore playlist tracks that exceed the max track limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimitPermittedForBypass(): void
|
||||
{
|
||||
$trackLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()
|
||||
->withPermissions(
|
||||
CrudPermission::DELETE()->format(PlaylistTrack::class),
|
||||
ExtendedCrudPermission::RESTORE()->format(PlaylistTrack::class),
|
||||
SpecialPermission::BYPASS_FEATURE_FLAGS
|
||||
)
|
||||
->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->tracks($trackLimit)
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$track = PlaylistTrack::factory()
|
||||
->for($playlist)
|
||||
->createOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$this->delete(route('api.playlist.track.destroy', ['playlist' => $playlist, 'track' => $track]));
|
||||
|
||||
$response = $this->patch(route('api.playlist.track.restore', ['playlist' => $playlist, 'track' => $track]));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Http\Api\List\Playlist\Track;
|
||||
|
||||
use App\Constants\Config\FlagConstants;
|
||||
use App\Constants\Config\PlaylistConstants;
|
||||
use App\Enums\Auth\CrudPermission;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Models\Auth\User;
|
||||
@@ -517,4 +518,72 @@ class TrackStoreTest extends TestCase
|
||||
|
||||
$response->assertCreated();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Track Store Endpoint shall forbid users from creating playlists that exceed the max track limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimit(): void
|
||||
{
|
||||
$trackLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(PlaylistTrack::class))->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->tracks($trackLimit)
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$track = PlaylistTrack::factory()
|
||||
->for($playlist)
|
||||
->for(Video::factory())
|
||||
->makeOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->post(route('api.playlist.track.store', ['playlist' => $playlist] + $track->toArray()));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* The Track Store Endpoint shall permit users with bypass feature flag permission
|
||||
* to create playlists that exceed the max track limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testMaxTrackLimitPermittedForBypass(): void
|
||||
{
|
||||
$trackLimit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
|
||||
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
|
||||
|
||||
$user = User::factory()
|
||||
->withPermissions(
|
||||
CrudPermission::CREATE()->format(PlaylistTrack::class),
|
||||
SpecialPermission::BYPASS_FEATURE_FLAGS
|
||||
)
|
||||
->createOne();
|
||||
|
||||
$playlist = Playlist::factory()
|
||||
->tracks($trackLimit)
|
||||
->for($user)
|
||||
->createOne();
|
||||
|
||||
$track = PlaylistTrack::factory()
|
||||
->for($playlist)
|
||||
->for(Video::factory())
|
||||
->makeOne();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->post(route('api.playlist.track.store', ['playlist' => $playlist] + $track->toArray()));
|
||||
|
||||
$response->assertCreated();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user