Update message when user disables category

This commit is contained in:
DariusIII
2026-02-04 13:47:36 +01:00
parent 8f5092182a
commit d5746d98e3
3 changed files with 379 additions and 41 deletions
+208 -36
View File
@@ -2,6 +2,8 @@
namespace App\Http\Middleware;
use App\Models\Category;
use App\Models\RootCategory;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -18,66 +20,236 @@ class ClearanceMiddleware
{
$user = $request->user();
if ($user->hasAnyRole(['Admin', 'Moderator']) && ! $request->is(['Admin', 'Admin/*'])) {
// Admin/Moderator bypass only applies to Admin section pages
// They should still respect their personal category permission settings
if ($request->is(['Admin', 'Admin/*'])) {
if (! $user->hasAnyRole(['Admin', 'Moderator'])) {
abort(403, 'Unauthorized access to admin area');
}
return $next($request);
}
if ($request->is(['Movies', 'browse/Movies', 'browse/Movies/*'])) {
if (! $user->hasDirectPermission('view movies') || ! $user->hasPermissionTo('view movies')) {
abort(401);
} else {
return $next($request);
// Get the path for pattern matching
$path = $request->path();
// Check for browse routes with subcategory - e.g., browse/Movies/HD
if (preg_match('#^browse/([^/]+)/([^/]+)$#i', $path, $matches)) {
$parentCategoryName = $matches[1];
$subcategoryName = $matches[2];
// First check if the main category is blocked
$blockedCategory = $this->checkMainCategoryPermission($user, $parentCategoryName);
if ($blockedCategory) {
return $this->abortCategoryDisabled($blockedCategory);
}
// Then check if the subcategory is excluded (unless it's "All")
if (strtolower($subcategoryName) !== 'all') {
$blockedSubcategory = $this->checkSubcategoryExclusion($user, $parentCategoryName, $subcategoryName);
if ($blockedSubcategory) {
return $this->abortSubcategoryDisabled($parentCategoryName, $blockedSubcategory);
}
}
return $next($request);
}
if ($request->is(['Console', 'browse/Console', 'browse/Console/*'])) {
if (! $user->hasDirectPermission('view console') || ! $user->hasPermissionTo('view console')) {
abort(401);
} else {
return $next($request);
// Check for browse routes without subcategory - e.g., browse/Movies
if (preg_match('#^browse/([^/]+)$#i', $path, $matches)) {
$parentCategoryName = $matches[1];
$blockedCategory = $this->checkMainCategoryPermission($user, $parentCategoryName);
if ($blockedCategory) {
return $this->abortCategoryDisabled($blockedCategory);
}
return $next($request);
}
if ($request->is(['Books', 'browse/Books', 'browse/Books/*'])) {
if (! $user->hasDirectPermission('view books') || ! $user->hasPermissionTo('view books')) {
abort(401);
} else {
return $next($request);
// Movies category
if ($this->matchesCategoryPath($path, 'Movies')
|| $this->matchesCategoryPath($path, 'movie')
|| $this->matchesCategoryPath($path, 'trending-movies')
|| $this->matchesCategoryPath($path, 'movietrailers')
|| $this->matchesCategoryPath($path, 'mymovies')) {
if (! $user->hasDirectPermission('view movies')) {
return $this->abortCategoryDisabled('Movies');
}
return $next($request);
}
if ($request->is(['Audio', 'browse/Audio', 'browse/Audio/*'])) {
if (! $user->hasDirectPermission('view audio') || ! $user->hasPermissionTo('view audio')) {
abort(401);
} else {
return $next($request);
// Console category
if ($this->matchesCategoryPath($path, 'Console')) {
if (! $user->hasDirectPermission('view console')) {
return $this->abortCategoryDisabled('Console');
}
return $next($request);
}
if ($request->is(['XXX', 'browse/XXX', 'browse/XXX/*'])) {
if (! $user->hasDirectPermission('view adult') || ! $user->hasPermissionTo('view adult')) {
abort(401);
} else {
return $next($request);
// Books category
if ($this->matchesCategoryPath($path, 'Books')) {
if (! $user->hasDirectPermission('view books')) {
return $this->abortCategoryDisabled('Books');
}
return $next($request);
}
if ($request->is(['Games', 'browse/PC', 'browse/PC/*'])) {
if (! $user->hasDirectPermission('view pc') || ! $user->hasPermissionTo('view pc')) {
abort(401);
} else {
return $next($request);
// Audio category
if ($this->matchesCategoryPath($path, 'Audio')) {
if (! $user->hasDirectPermission('view audio')) {
return $this->abortCategoryDisabled('Audio');
}
return $next($request);
}
if ($request->is(['TV', 'browse/TV', 'browse/TV/*'])) {
if (! $user->hasDirectPermission('view tv') || ! $user->hasPermissionTo('view tv')) {
abort(401);
} else {
return $next($request);
// Adult (XXX) category
if ($this->matchesCategoryPath($path, 'XXX')) {
if (! $user->hasDirectPermission('view adult')) {
return $this->abortCategoryDisabled('Adult');
}
return $next($request);
}
// PC/Games category
if ($this->matchesCategoryPath($path, 'Games') || $this->matchesCategoryPath($path, 'PC')) {
if (! $user->hasDirectPermission('view pc')) {
return $this->abortCategoryDisabled('PC');
}
return $next($request);
}
// TV category
if ($this->matchesCategoryPath($path, 'TV')
|| $this->matchesCategoryPath($path, 'series')
|| $this->matchesCategoryPath($path, 'trending-tv')
|| $this->matchesCategoryPath($path, 'myshows')) {
if (! $user->hasDirectPermission('view tv')) {
return $this->abortCategoryDisabled('TV');
}
return $next($request);
}
return $next($request);
}
/**
* Check if the user has permission to view a main category.
*
* @return string|null The blocked category name, or null if allowed
*/
protected function checkMainCategoryPermission($user, string $parentCategoryName): ?string
{
$categoryPermissions = [
'movies' => 'view movies',
'console' => 'view console',
'books' => 'view books',
'audio' => 'view audio',
'xxx' => 'view adult',
'games' => 'view pc',
'pc' => 'view pc',
'tv' => 'view tv',
];
$categoryDisplayNames = [
'movies' => 'Movies',
'console' => 'Console',
'books' => 'Books',
'audio' => 'Audio',
'xxx' => 'Adult',
'games' => 'PC',
'pc' => 'PC',
'tv' => 'TV',
];
$lowerName = strtolower($parentCategoryName);
if (isset($categoryPermissions[$lowerName])) {
if (! $user->hasDirectPermission($categoryPermissions[$lowerName])) {
return $categoryDisplayNames[$lowerName];
}
}
return null;
}
/**
* Check if the user has excluded a specific subcategory.
*
* @return string|null The blocked subcategory name, or null if allowed
*/
protected function checkSubcategoryExclusion($user, string $parentCategoryName, string $subcategoryName): ?string
{
// Get the root category ID
$rootCategory = RootCategory::query()
->whereRaw('LOWER(title) = ?', [strtolower($parentCategoryName)])
->first();
if (! $rootCategory) {
return null;
}
// Get the subcategory
$subcategory = Category::query()
->where('root_categories_id', $rootCategory->id)
->whereRaw('LOWER(title) = ?', [strtolower($subcategoryName)])
->first();
if (! $subcategory) {
return null;
}
// Check if this subcategory is in the user's exclusion list
$isExcluded = $user->excludedCategories()
->where('categories_id', $subcategory->id)
->exists();
if ($isExcluded) {
return $subcategory->title;
}
return null;
}
/**
* Check if the path matches a category pattern (case-insensitive).
*/
protected function matchesCategoryPath(string $path, string $category): bool
{
$lowerPath = strtolower($path);
$lowerCategory = strtolower($category);
// Match: Category, Category/*, browse/Category, browse/Category/*
return $lowerPath === $lowerCategory
|| str_starts_with($lowerPath, $lowerCategory.'/')
|| $lowerPath === 'browse/'.$lowerCategory
|| str_starts_with($lowerPath, 'browse/'.$lowerCategory.'/');
}
/**
* Abort with a category disabled response.
*/
protected function abortCategoryDisabled(string $category): Response
{
return response()->view('errors.category-disabled', [
'category' => $category,
], 403);
}
/**
* Abort with a subcategory disabled response.
*/
protected function abortSubcategoryDisabled(string $parentCategory, string $subcategory): Response
{
return response()->view('errors.category-disabled', [
'category' => $parentCategory.' - '.$subcategory,
'isSubcategory' => true,
], 403);
}
}
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ __('Category Disabled') }}</title>
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}code{font-family:monospace,monospace;font-size:1em}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}
</style>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
}
@media (prefers-color-scheme: dark) {
.container {
background-color: #1f2937;
}
.card {
background-color: #374151;
}
.title {
color: #f9fafb;
}
.message {
color: #d1d5db;
}
.icon-container {
background-color: #fef3c7;
}
}
.card {
max-width: 28rem;
margin: 1rem;
padding: 2rem;
background-color: #ffffff;
border-radius: 0.75rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
text-align: center;
}
.icon-container {
width: 4rem;
height: 4rem;
margin: 0 auto 1.5rem;
border-radius: 9999px;
background-color: #fef3c7;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
width: 2rem;
height: 2rem;
color: #f59e0b;
}
.title {
font-size: 1.5rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.5rem;
}
.category-name {
color: #3b82f6;
}
.message {
color: #6b7280;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: #ffffff;
font-weight: 600;
border-radius: 0.5rem;
text-decoration: none;
transition: background-color 0.2s ease;
}
.button:hover {
background-color: #2563eb;
}
.back-link {
display: block;
margin-top: 1rem;
color: #6b7280;
text-decoration: underline;
font-size: 0.875rem;
}
.back-link:hover {
color: #4b5563;
}
@media (prefers-color-scheme: dark) {
.back-link {
color: #9ca3af;
}
.back-link:hover {
color: #d1d5db;
}
}
</style>
</head>
<body class="antialiased">
<div class="container">
<div class="card">
<div class="icon-container">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h1 class="title">
<span class="category-name">{{ $category }}</span> {{ isset($isSubcategory) && $isSubcategory ? 'Subcategory' : 'Category' }} Disabled
</h1>
<p class="message">
@if(isset($isSubcategory) && $isSubcategory)
You have excluded the <strong>{{ $category }}</strong> subcategory in your profile settings.
To access this content, please remove this subcategory from your exclusions in your profile.
@else
You have disabled viewing of the <strong>{{ $category }}</strong> category in your profile settings.
To access this content, please enable the category in your profile.
@endif
</p>
<a href="{{ url('profileedit') }}" class="button">
Go to Profile Settings
</a>
<a href="{{ url('/') }}" class="back-link">
Back to Home
</a>
</div>
</div>
</body>
</html>
+7 -5
View File
@@ -143,6 +143,13 @@ Route::middleware('isVerified')->group(function () {
Route::match(['GET', 'POST'], 'Console/{id?}', [ConsoleController::class, 'show'])->name('Console');
Route::match(['GET', 'POST'], 'XXX/{id?}', [AdultController::class, 'show'])->name('XXX');
Route::match(['GET', 'POST'], 'Books/{id?}', [BooksController::class, 'index'])->name('Books');
// TV-related routes
Route::match(['GET', 'POST'], 'series/{id?}', [SeriesController::class, 'index'])->name('series');
Route::match(['GET', 'POST'], 'trending-tv', [SeriesController::class, 'showTrending'])->name('trending-tv');
Route::match(['GET', 'POST'], 'myshows', [MyShowsController::class, 'show'])->name('myshows');
Route::match(['GET', 'POST'], 'myshows/browse', [MyShowsController::class, 'browse'])->name('myshows.browse');
// Movies-related routes
Route::match(['GET', 'POST'], 'mymovies', [MyMoviesController::class, 'show'])->name('mymovies');
});
Route::match(['GET', 'POST'], 'nfo/{id?}', [NfoController::class, 'showNfo'])->name('nfo');
@@ -159,12 +166,7 @@ Route::middleware('isVerified')->group(function () {
Route::get('release-report/reasons', [\App\Http\Controllers\ReleaseReportController::class, 'getReasons'])->name('release-report.reasons');
Route::get('release-report/check', [\App\Http\Controllers\ReleaseReportController::class, 'checkReported'])->name('release-report.check');
Route::match(['GET', 'POST'], 'mymovies', [MyMoviesController::class, 'show'])->name('mymovies');
Route::match(['GET', 'POST'], 'myshows', [MyShowsController::class, 'show'])->name('myshows');
Route::match(['GET', 'POST'], 'myshows/browse', [MyShowsController::class, 'browse'])->name('myshows.browse');
Route::get('api/release/{guid}/filelist', [\App\Http\Controllers\Api\FileListApiController::class, 'getFileList'])->name('api.filelist');
Route::match(['GET', 'POST'], 'series/{id?}', [SeriesController::class, 'index'])->name('series');
Route::match(['GET', 'POST'], 'trending-tv', [SeriesController::class, 'showTrending'])->name('trending-tv');
Route::match(['GET', 'POST'], 'ajax_profile', [AjaxController::class, 'profile'])->name('ajax_profile');
Route::match(['GET', 'POST'], '2fa', [PasswordSecurityController::class, 'show2faForm'])->name('2fa');
Route::get('2fa/enable', [PasswordSecurityController::class, 'showEnable2faForm'])->name('2fa.enable');