Compare commits

...

5 Commits

Author SHA1 Message Date
af739e3c88 Remove old comments repo in composer.json 2026-01-18 18:49:59 +01:00
273ed65a8d Remove laravel sail 2026-01-18 18:44:59 +01:00
ccfd5b996b Replace captcha system 2026-01-18 18:37:08 +01:00
e5ef197ed6 Add user roles system 2026-01-16 23:14:47 +01:00
c0be2e294a Refactor routes 2026-01-16 23:01:34 +01:00
39 changed files with 563 additions and 596 deletions

11
app/Enums/UserRole.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserRole: string
{
case ADMINISTRATOR = 'admin';
case MODERATOR = 'moderator';
case SUPPORTER = 'supporter';
case BANNED = 'banned';
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Models\User; use App\Models\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -31,11 +32,11 @@ class UserController extends Controller
switch ($validated['action']) { switch ($validated['action']) {
case 'ban': case 'ban':
$user->update(['is_banned' => 1]); $user->addRole(UserRole::BANNED);
alert()->success('Banned', 'User has been banned.'); alert()->success('Banned', 'User has been banned.');
break; break;
case 'unban': case 'unban':
$user->update(['is_banned' => 0]); $user->removeRole(UserRole::BANNED);
alert()->success('Unbanned', 'User has been unbanned.'); alert()->success('Unbanned', 'User has been unbanned.');
break; break;
default: default:

View File

@@ -8,6 +8,8 @@ use App\Models\Episode;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use GrantHolle\Altcha\Rules\ValidAltcha;
class DownloadApiController extends Controller class DownloadApiController extends Controller
{ {
/** /**
@@ -16,11 +18,12 @@ class DownloadApiController extends Controller
public function getDownload(Request $request) public function getDownload(Request $request)
{ {
$validated = $request->validate([ $validated = $request->validate([
'episode_id' => 'required', 'episode_id' => ['required'],
'captcha' => 'required|captcha' 'captcha' => ['required', new ValidAltcha],
]); ]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail(); $episode = Episode::where('id', $request->input('episode_id'))
->firstOrFail();
// Increase download count, as we assume the user // Increase download count, as we assume the user
// downloads after submitting the captcha // downloads after submitting the captcha

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Enums\UserRole;
use App\Models\User; use App\Models\User;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@@ -88,10 +89,7 @@ class DiscordAuthController extends Controller
// User is not in the guild // User is not in the guild
if ($response->status() === 404) { if ($response->status() === 404) {
$user->update([ $user->removeRole(UserRole::SUPPORTER);
'is_patreon' => false,
]);
return; return;
} }
@@ -110,21 +108,15 @@ class DiscordAuthController extends Controller
$discordRoles = $response->json('roles', []); $discordRoles = $response->json('roles', []);
$patreonRoles = config('discord.patreon_roles', []); $patreonRoles = config('discord.patreon_roles', []);
$isPatreon = false; // If intersect of array is empty, then the user doesn't have the role
foreach($patreonRoles as $patreonRole) $hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
{
if (in_array($patreonRole, $discordRoles, true)) { if (!$hasSupporterRole) {
$isPatreon = true; // Remove role if not found
break; $user->removeRole(UserRole::SUPPORTER);
} return;
} }
// Only update if something actually changed $user->addRole(UserRole::SUPPORTER);
if ($user->is_patreon !== $isPatreon) {
$user->update([
'is_patreon' => $isPatreon,
]);
} }
}
} }

View File

@@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules; use Illuminate\Validation\Rules;
use GrantHolle\Altcha\Rules\ValidAltcha;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
{ {
/** /**
@@ -24,6 +26,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()], 'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidAltcha],
]); ]);
$user = User::create([ $user = User::create([

View File

@@ -5,6 +5,8 @@ namespace App\Http\Controllers;
use App\Models\Contact; use App\Models\Contact;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use GrantHolle\Altcha\Rules\ValidAltcha;
class ContactController extends Controller class ContactController extends Controller
{ {
/** /**
@@ -25,7 +27,7 @@ class ContactController extends Controller
'email' => 'required|max:50', 'email' => 'required|max:50',
'message' => 'required|max:1000', 'message' => 'required|max:1000',
'subject' => 'required|max:50', 'subject' => 'required|max:50',
'captcha' => 'required|captcha', 'altcha' => ['required', new ValidAltcha],
]); ]);
$contact = new Contact(); $contact = new Contact();
@@ -37,9 +39,4 @@ class ContactController extends Controller
return back()->with('status', 'contact-submitted'); return back()->with('status', 'contact-submitted');
} }
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
{
return response()->json(['captcha'=> captcha_img()]);
}
} }

View File

@@ -59,6 +59,7 @@ class Kernel extends HttpKernel
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class, 'auth.admin' => \App\Http\Middleware\IsAdmin::class,
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,

View File

@@ -1,28 +1,14 @@
<?php namespace app\Http\Middleware; <?php namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin { class IsAdmin {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
@@ -30,15 +16,14 @@ class IsAdmin {
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
if( ! $this->auth->user()->is_admin) if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
{ {
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
return $next($request); return $next($request);
} }
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
} }

View File

@@ -1,8 +1,11 @@
<?php namespace app\Http\Middleware; <?php namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure; use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Guard; use Symfony\Component\HttpFoundation\Response;
class IsBanned { class IsBanned {
@@ -13,9 +16,9 @@ class IsBanned {
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): Response
{ {
if(auth()->check() && auth()->user()->is_banned == 1) if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
{ {
Auth::logout(); Auth::logout();
$request->session()->invalidate(); $request->session()->invalidate();

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsModerator
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
{
return $next($request);
}
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
}

View File

@@ -9,6 +9,8 @@ use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use GrantHolle\Altcha\Rules\ValidAltcha;
class LoginRequest extends FormRequest class LoginRequest extends FormRequest
{ {
/** /**
@@ -29,6 +31,7 @@ class LoginRequest extends FormRequest
return [ return [
'email' => ['required', 'string', 'email'], 'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'], 'password' => ['required', 'string'],
'altcha' => ['required', new ValidAltcha],
]; ];
} }

View File

@@ -2,6 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment; use App\Models\Comment;
use App\Models\User; use App\Models\User;
@@ -17,7 +18,7 @@ class AdminUserSearch extends Component
public $search = ''; public $search = '';
#[Url(history: true)] #[Url(history: true)]
public $filtered = ['true']; public $discordId = '';
#[Url(history: true)] #[Url(history: true)]
public $patreon = []; public $patreon = [];
@@ -38,10 +39,10 @@ class AdminUserSearch extends Component
public function render() public function render()
{ {
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000)) $users = User::when($this->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1)) ->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%')) ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
->paginate(20); ->paginate(20);
return view('livewire.admin-user-search', [ return view('livewire.admin-user-search', [

View File

@@ -73,9 +73,9 @@ class DownloadsSearch extends Component
$types[] = 'FHD'; $types[] = 'FHD';
} elseif ($label === 'FHD 48fps') { } elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi'; $types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->is_patreon) { } elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
$types[] = 'UHD'; $types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->is_patreon) { } elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
$types[] = 'UHDi'; $types[] = 'UHDi';
} }
} }
@@ -98,7 +98,7 @@ class DownloadsSearch extends Component
public function mount() public function mount()
{ {
if (!auth()->user()->is_patreon) { if (!auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
return; return;
} }

View File

@@ -3,6 +3,8 @@
namespace App\Models; namespace App\Models;
//use Illuminate\Contracts\Auth\MustVerifyEmail; //use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -25,7 +27,6 @@ class User extends Authenticatable
'email', 'email',
'password', 'password',
'locale', 'locale',
'is_banned',
// Discord // Discord
'discord_id', 'discord_id',
'discord_avatar', 'discord_avatar',
@@ -54,7 +55,7 @@ class User extends Authenticatable
'name' => 'string', 'name' => 'string',
'email' => 'string', 'email' => 'string',
'locale' => 'string', 'locale' => 'string',
'roles' => 'json', 'roles' => 'array',
'tag_blacklist' => 'array', 'tag_blacklist' => 'array',
// Discord // Discord
'discord_id' => 'integer', 'discord_id' => 'integer',
@@ -119,4 +120,44 @@ class User extends Authenticatable
return asset('images/default-avatar.webp'); return asset('images/default-avatar.webp');
} }
/**
* Check if user has a specific role
*/
public function hasRole(UserRole $role): bool
{
return in_array($role->value, $this->roles ?? [], true);
}
/**
* Add Role to User
*/
public function addRole(UserRole $role): void
{
if ($this->hasRole($role)) {
return;
}
// Get all current roles
$roles = $this->roles ?? [];
// Add new role
$roles[] = $role->value;
$this->roles = $roles;
$this->save();
}
/**
* Remove Role from User
*/
public function removeRole(UserRole $role): void
{
if (!$this->hasRole($role)) {
return;
}
$this->roles = array_diff($this->roles, [$role->value]);
$this->save();
}
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "laravel/laravel", "name": "w33b/hstream",
"type": "project", "type": "project",
"description": "The skeleton application for the Laravel framework.", "description": "The website of hstream.moe",
"keywords": [ "keywords": [
"laravel", "laravel",
"framework" "framework"
@@ -9,6 +9,7 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"grantholle/laravel-altcha": "^2.1",
"guzzlehttp/guzzle": "^7.8.1", "guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0", "hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2", "http-interop/http-factory-guzzle": "^1.2",
@@ -22,7 +23,6 @@
"livewire/livewire": "^3.7.0", "livewire/livewire": "^3.7.0",
"maize-tech/laravel-markable": "^2.3.0", "maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16", "meilisearch/meilisearch-php": "^1.16",
"mews/captcha": "^3.4.4",
"predis/predis": "^2.2", "predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2", "realrashid/sweet-alert": "^7.2",
"rtconner/laravel-tagging": "^5.0", "rtconner/laravel-tagging": "^5.0",
@@ -35,18 +35,12 @@
"fakerphp/faker": "^1.24.0", "fakerphp/faker": "^1.24.0",
"laravel/breeze": "^2.3", "laravel/breeze": "^2.3",
"laravel/pint": "^1.18", "laravel/pint": "^1.18",
"laravel/sail": "^1.38",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1", "nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.4", "phpunit/phpunit": "^11.4",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },
"repositories": [ "repositories": [],
{
"type": "vcs",
"url": "https://github.com/renatokira/comments.git"
}
],
"autoload": { "autoload": {
"exclude-from-classmap": [], "exclude-from-classmap": [],
"psr-4": { "psr-4": {

339
composer.lock generated
View File

@@ -4,8 +4,55 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9287e7ef1f943600ac3e2b78bc9cd7c8", "content-hash": "2e359f5cfd56c822e336b74becc9c9d9",
"packages": [ "packages": [
{
"name": "altcha-org/altcha",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/altcha-org/altcha-lib-php.git",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/altcha-org/altcha-lib-php/zipball/9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"reference": "9e9e70c864a9db960d071c77c778be0c9ff1a4d0",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=8.2"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.72",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^11.5"
},
"type": "library",
"autoload": {
"psr-4": {
"AltchaOrg\\Altcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Regeci",
"email": "536331+ovx@users.noreply.github.com"
}
],
"support": {
"issues": "https://github.com/altcha-org/altcha-lib-php/issues",
"source": "https://github.com/altcha-org/altcha-lib-php/tree/v1.3.1"
},
"time": "2025-12-13T10:03:53+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.1", "version": "0.14.1",
@@ -776,6 +823,82 @@
], ],
"time": "2025-12-27T19:43:20+00:00" "time": "2025-12-27T19:43:20+00:00"
}, },
{
"name": "grantholle/laravel-altcha",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/grantholle/laravel-altcha.git",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grantholle/laravel-altcha/zipball/c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"reference": "c0dcc6d0805e8640d46709e5f8d05c7c65b2687c",
"shasum": ""
},
"require": {
"altcha-org/altcha": "^1.3.1",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.1.1||^7.10.0",
"orchestra/testbench": "^10.0.0||^9.0.0||^8.22.0",
"pestphp/pest": "^3.0||^2.0",
"pestphp/pest-plugin-arch": "^3.0||^2.0",
"pestphp/pest-plugin-laravel": "^3.0||^2.0",
"spatie/laravel-ray": "^1.26"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Altcha": "GrantHolle\\Altcha\\Facades\\Altcha"
},
"providers": [
"GrantHolle\\Altcha\\AltchaServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"GrantHolle\\Altcha\\": "src/",
"GrantHolle\\Altcha\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grant Holle",
"email": "hollegrant@gmail.com",
"role": "Developer"
}
],
"description": "A Laravel server implementation for Altcha.",
"homepage": "https://github.com/grantholle/laravel-altcha",
"keywords": [
"Grant Holle",
"laravel",
"laravel-altcha"
],
"support": {
"issues": "https://github.com/grantholle/laravel-altcha/issues",
"source": "https://github.com/grantholle/laravel-altcha/tree/2.1.1"
},
"funding": [
{
"url": "https://github.com/Grant Holle",
"type": "github"
}
],
"time": "2025-12-16T03:39:06+00:00"
},
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
"version": "7.10.0", "version": "7.10.0",
@@ -3216,79 +3339,6 @@
}, },
"time": "2025-09-18T10:15:45+00:00" "time": "2025-09-18T10:15:45+00:00"
}, },
{
"name": "mews/captcha",
"version": "3.4.7",
"source": {
"type": "git",
"url": "https://github.com/mewebstudio/captcha.git",
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mewebstudio/captcha/zipball/2622c4f90dd621f19fe57e03e45f6f099509e839",
"reference": "2622c4f90dd621f19fe57e03e45f6f099509e839",
"shasum": ""
},
"require": {
"ext-gd": "*",
"illuminate/config": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/filesystem": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/hashing": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/session": "~5|^6|^7|^8|^9|^10|^11|^12",
"illuminate/support": "~5|^6|^7|^8|^9|^10|^11|^12",
"intervention/image": "^3.7",
"php": "^7.2|^8.1|^8.2|^8.3"
},
"require-dev": {
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^8.5|^9.5.10|^10.5|^11"
},
"type": "package",
"extra": {
"laravel": {
"aliases": {
"Captcha": "Mews\\Captcha\\Facades\\Captcha"
},
"providers": [
"Mews\\Captcha\\CaptchaServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Mews\\Captcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Muharrem ERİN",
"email": "me@mewebstudio.com",
"homepage": "https://github.com/mewebstudio",
"role": "Developer"
}
],
"description": "Laravel 5/6/7/8/9/10/11/12 Captcha Package",
"homepage": "https://github.com/mewebstudio/captcha",
"keywords": [
"captcha",
"laravel12 Captcha",
"laravel12 Security",
"laravel5 Security"
],
"support": {
"issues": "https://github.com/mewebstudio/captcha/issues",
"source": "https://github.com/mewebstudio/captcha/tree/3.4.7"
},
"time": "2025-10-11T14:42:33+00:00"
},
{ {
"name": "mobiledetect/mobiledetectlib", "name": "mobiledetect/mobiledetectlib",
"version": "4.8.10", "version": "4.8.10",
@@ -9177,69 +9227,6 @@
}, },
"time": "2026-01-05T16:49:17+00:00" "time": "2026-01-05T16:49:17+00:00"
}, },
{
"name": "laravel/sail",
"version": "v1.52.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
"reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"shasum": ""
},
"require": {
"illuminate/console": "^9.52.16|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0",
"illuminate/support": "^9.52.16|^10.0|^11.0|^12.0",
"php": "^8.0",
"symfony/console": "^6.0|^7.0",
"symfony/yaml": "^6.0|^7.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpstan/phpstan": "^2.0"
},
"bin": [
"bin/sail"
],
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sail\\SailServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sail\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Docker files for running a basic Laravel application.",
"keywords": [
"docker",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
"time": "2026-01-01T02:46:03+00:00"
},
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
"version": "1.6.12", "version": "1.6.12",
@@ -11537,82 +11524,6 @@
], ],
"time": "2024-10-20T05:08:20+00:00" "time": "2024-10-20T05:08:20+00:00"
}, },
{
"name": "symfony/yaml",
"version": "v7.4.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "24dd4de28d2e3988b311751ac49e684d783e2345"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345",
"reference": "24dd4de28d2e3988b311751ac49e684d783e2345",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"symfony/console": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0|^8.0"
},
"bin": [
"Resources/bin/yaml-lint"
],
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.4.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-12-04T18:11:45+00:00"
},
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.3.1", "version": "1.3.1",
@@ -11673,5 +11584,5 @@
"php": "^8.2" "php": "^8.2"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.6.0" "plugin-api-version": "2.9.0"
} }

View File

@@ -1,50 +0,0 @@
<?php
return [
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
'default' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => false,
'expire' => 60,
'encrypt' => false,
],
'math' => [
'length' => 9,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => true,
],
'flat' => [
'length' => 6,
'width' => 160,
'height' => 46,
'quality' => 90,
'lines' => 6,
'bgImage' => false,
'bgColor' => '#ecf2f4',
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
'contrast' => -5,
],
'mini' => [
'length' => 3,
'width' => 60,
'height' => 32,
],
'inverse' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'sensitive' => true,
'angle' => 12,
'sharpen' => 10,
'blur' => 2,
'invert' => true,
'contrast' => -5,
]
];

View File

@@ -6,13 +6,13 @@ return [
'guild_id' => 802233383710228550, 'guild_id' => 802233383710228550,
'patreon_roles' => [ 'patreon_roles' => [
841798154999169054, // ???? '841798154999169054', // ????
803329707650187364, // Tier-5 '803329707650187364', // Tier-5
803327903659196416, // ???? '803327903659196416', // ????
803325441942356059, // Tier-3 '803325441942356059', // Tier-3
803322725576736858, // Tier-2 '803322725576736858', // Tier-2
802270568912519198, // Tier-1 '802270568912519198', // Tier-1
802234830384267315 // admin '802234830384267315' // admin
], ],
'discord_bot_token' => env('DISCORD_BOT_TOKEN'), 'discord_bot_token' => env('DISCORD_BOT_TOKEN'),

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Migrate supporters
DB::table('users')->where('is_patreon', 1)->update([
'roles' => DB::raw("JSON_ARRAY('supporter')")
]);
// Migrate banned
DB::table('users')->where('is_banned', 1)->update([
'roles' => DB::raw("JSON_ARRAY('banned')")
]);
// Migrate admins
DB::table('users')->where('is_admin', 1)->update([
'roles' => DB::raw("JSON_ARRAY('admin')")
]);
// Drop columns
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
$table->dropColumn('is_patreon');
$table->dropColumn('is_banned');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('users')->update(['roles' => null]);
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(0);
$table->boolean('is_patreon')->default(0);
$table->boolean('is_banned')->default(0);
});
}
};

View File

@@ -1,74 +0,0 @@
services:
laravel.test:
build:
context: './vendor/laravel/sail/runtimes/8.3'
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
MYSQL_CLIENT: mariadb-client
image: 'sail-8.3/app'
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- '.:/var/www/html'
networks:
- sail
depends_on:
- mariadb
- redis
mariadb:
image: 'mariadb:11'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
- 'sail-mariadb:/var/lib/mysql'
- './vendor/laravel/sail/database/mariadb/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- healthcheck.sh
- '--connect'
- '--innodb_initialized'
retries: 3
timeout: 5s
redis:
image: 'redis:alpine'
ports:
- '${FORWARD_REDIS_PORT:-6379}:6379'
volumes:
- 'sail-redis:/data'
networks:
- sail
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-mariadb:
driver: local
sail-redis:
driver: local

50
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1", "@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2", "@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"dashjs": "^5.0.0", "dashjs": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@@ -40,6 +41,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@altcha/crypto": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@altcha/crypto/-/crypto-0.0.1.tgz",
"integrity": "sha512-qZMdnoD3lAyvfSUMNtC2adRi666Pxdcw9zqfMU5qBOaJWqpN9K+eqQGWqeiKDMqL0SF+EytNG4kR/Pr/99GJ6g==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -1161,6 +1168,31 @@
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
}, },
"node_modules/altcha": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/altcha/-/altcha-2.3.0.tgz",
"integrity": "sha512-vl8I0dQvSQB7/Mx09XuWZ1+LdSP7vEda6OLbg9kUQ2ZO2LT7MzgUyLK7Iips+GAV6c0ntVcS1XWOqhEPpwbDhQ==",
"license": "MIT",
"dependencies": {
"@altcha/crypto": "^0.0.1"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.18.0"
}
},
"node_modules/altcha/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/any-promise": { "node_modules/any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -3064,6 +3096,15 @@
"postcss": "^8.0.9" "postcss": "^8.0.9"
} }
}, },
"node_modules/tw-elements/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.41", "version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
@@ -3302,15 +3343,6 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT" "license": "MIT"
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
} }
} }
} }

View File

@@ -20,6 +20,7 @@
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1", "@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2", "@yaireo/tagify": "^4.21.2",
"altcha": "^2.3.0",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"dashjs": "^5.0.0", "dashjs": "^5.0.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",

View File

@@ -123,3 +123,32 @@ input:checked~.dot {
src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff'); src: url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff2) format('woff2'), url(https://fonts.bunny.net/figtree/files/figtree-latin-ext-600-normal.woff) format('woff');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* Captcha */
:root {
--altcha-border-width: 1px;
--altcha-border-radius: 0.375rem;
--altcha-color-base: #333;
--altcha-color-border: #a0a0a0;
--altcha-color-text: #fff;
--altcha-color-border-focus: currentColor;
--altcha-color-error-text: #f23939;
--altcha-color-footer-bg: #141414;
--altcha-max-width: 260px;
}
.altcha-footer {
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
}
input[type="checkbox"] {
background-color: #ffffff;
border-color: #a0a0a0;
color: rgb(225,29,72);
}
input[type="checkbox"]:checked {
background-color: rgb(225,29,72);
box-shadow: 0 0 0 0px #fff, 0 0 0 calc(2px + 0px) rgba(246, 59, 118, 0.5), 0 0 #0000;
}

View File

@@ -12,6 +12,9 @@ import {
initTE, initTE,
} from "tw-elements"; } from "tw-elements";
// Captcha
import 'altcha';
// import Alpine from 'alpinejs'; // import Alpine from 'alpinejs';
// window.Alpine = Alpine; // window.Alpine = Alpine;

View File

@@ -6,7 +6,7 @@
<div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert"> <div class="mb-4 rounded-lg bg-success-400 px-6 py-5 text-base text-success-800 mt-5" role="alert">
{{ $alert->text }} {{ $alert->text }}
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900"> <form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-success-900">
@csrf @csrf
@method('delete') @method('delete')
@@ -21,7 +21,7 @@
<div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert"> <div class="mb-4 rounded-lg bg-danger-400 px-6 py-5 text-base text-danger-800 mt-5" role="alert">
{{ $alert->text }} {{ $alert->text }}
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900"> <form method="POST" action="{{ route('admin.alert.delete', $alert->id) }}" class="float-right hover:text-danger-900">
@csrf @csrf
@method('delete') @method('delete')

View File

@@ -1,5 +1,5 @@
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10"> <div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
<div class="float-left"> <div class="float-left">
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap"> <a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">

View File

@@ -69,6 +69,11 @@
</label> </label>
</div> </div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
@if (Route::has('password.request')) @if (Route::has('password.request'))
<a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}"> <a class="underline text-sm text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 dark:focus:ring-offset-neutral-800" href="{{ route('password.request') }}">
@@ -127,6 +132,11 @@
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" /> <x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div> </div>
<div class="block">
<altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-error :messages="$errors->get('altcha')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<x-primary-button class="ms-4"> <x-primary-button class="ms-4">
{{ __('Register') }} {{ __('Register') }}

View File

@@ -33,19 +33,7 @@
<x-input-error class="mt-2" :messages="$errors->get('message')" /> <x-input-error class="mt-2" :messages="$errors->get('message')" />
</div> </div>
<div> <altcha-widget id="captcha" floating challengeurl="/altcha-challenge"></altcha-widget>
<x-input-label for="message" :value="__('Captcha')" />
<div class="flex pt-2">
<div id="captchaImg">
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<br>
<x-text-input id="captcha" class="block " type="text" name="captcha" required />
</div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<x-primary-button>{{ __('Submit') }}</x-primary-button> <x-primary-button>{{ __('Submit') }}</x-primary-button>
@@ -65,18 +53,4 @@
@endif @endif
</div> </div>
</form> </form>
<script>
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function(response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function(error) {
console.log(error);
});
}
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha);
</script>
</section> </section>

View File

@@ -163,7 +163,7 @@
<i class="fa-solid fa-gear"></i> {{ __('nav.settings') }} <i class="fa-solid fa-gear"></i> {{ __('nav.settings') }}
</x-dropdown-link> </x-dropdown-link>
@if (Auth::user()->is_admin) @if (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<x-dropdown-link href="{{ route('admin.upload.index') }}"> <x-dropdown-link href="{{ route('admin.upload.index') }}">
<i class="fa-solid fa-user-tie"></i> Admin <i class="fa-solid fa-user-tie"></i> Admin
</x-dropdown-link> </x-dropdown-link>

View File

@@ -6,12 +6,16 @@
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 "> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-pink-700 dark:text-neutral-200 ">
<tr> <tr>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
Discord-ID ID
</th>
<th scope="col" class="px-6 py-3">
Discord ID
<input <input
class="w-4 h-4 ml-2 text-rose-600 bg-gray-100 border-gray-300 rounded focus:ring-rose-500 dark:focus:ring-rose-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" wire:model.live.debounce.600ms="discordId"
type="checkbox" type="search"
wire:model.live="filtered" id="discord-search"
value="true" class="ml-2 w-32 h-7 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900"
placeholder="Search..."
> >
</th> </th>
<th scope="col" class="px-6 py-3"> <th scope="col" class="px-6 py-3">
@@ -59,14 +63,17 @@
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"> <th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ $user->id }} {{ $user->id }}
</th> </th>
<td class="px-6 py-4">
{{ $user->discord_id ?? 'n/a' }}
</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->name }} {{ $user->name }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->is_patreon ? 'Yes' : 'No' }} {{ $user->hasRole(\App\Enums\UserRole::SUPPORTER) ? 'Yes' : 'No' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->is_banned ? 'Yes' : 'No' }} {{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Yes' : 'No' }}
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ $user->created_at->format('Y-m-d') }} {{ $user->created_at->format('Y-m-d') }}
@@ -78,9 +85,9 @@
<form method="POST" action="{{ route('admin.user.update') }}"> <form method="POST" action="{{ route('admin.user.update') }}">
@csrf @csrf
<input type="hidden" value="{{ $user->id }}" name="id"> <input type="hidden" value="{{ $user->id }}" name="id">
<input type="hidden" value="{{ $user->is_banned ? 'unban' : 'ban' }}" name="action"> <input type="hidden" value="{{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'unban' : 'ban' }}" name="action">
<button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600"> <button type="submit" class="inline-block w-full rounded bg-rose-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">
{{ $user->is_banned ? 'Unban' : 'Ban' }} {{ $user->hasRole(\App\Enums\UserRole::BANNED) ? 'Unban' : 'Ban' }}
</button> </button>
</form> </form>
<button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600"> <button wire:click="deleteUserComments('{{ $user->id }}')" class="inline-block w-full rounded bg-red-600 pl-[4px] pr-[4px] p-[1px] text-xs font-medium uppercase leading-normal text-white transition duration-150 ease-in-out hover:bg-rose-700 focus:bg-rose-600">

View File

@@ -6,10 +6,10 @@
<div class="flex-grow"> <div class="flex-grow">
<div class="flex gap-2"> <div class="flex gap-2">
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p> <p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($comment->user->is_admin) @if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a> <a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif @endif
@if($comment->user->is_patreon) @if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a> <a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
@endif @endif
</div> </div>

View File

@@ -52,7 +52,7 @@
<br> <br>
@php $download = $episode->getDownloadByType('UHD'); @endphp @php $download = $episode->getDownloadByType('UHD'); @endphp
@isset($download) @isset($download)
@if (!Auth::user()->is_patreon) @if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
@if (config('hstream.free_downloads')) @if (config('hstream.free_downloads'))
<p class="font-bold text-gray-800 dark:text-gray-200"> <p class="font-bold text-gray-800 dark:text-gray-200">
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k <i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k
@@ -91,7 +91,7 @@
<br> <br>
@if ($episode->interpolated_uhd) @if ($episode->interpolated_uhd)
@if (!Auth::user()->is_patreon) @if (!Auth::user()->hasRole(\App\Enums\UserRole::SUPPORTER))
@if (config('hstream.free_downloads')) @if (config('hstream.free_downloads'))
<p class="font-bold text-gray-800 dark:text-gray-200"> <p class="font-bold text-gray-800 dark:text-gray-200">
<i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps <i class="fa-solid fa-lock-open pr-[4px] text-yellow-600"></i> 4k 48fps

View File

@@ -8,18 +8,7 @@
<p id="message" class="text-red-600"> <p id="message" class="text-red-600">
</p> </p>
<div class="flex pt-2"> <div class="flex pt-2">
<div id="captchaImg"> <altcha-widget id="altcha" challengeurl="/altcha-challenge"></altcha-widget>
{!! captcha_img() !!}
</div>
<button type="button" class="inline-flex items-center ml-2 px-2 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150" id="reloadcaptcha" >
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="flex pt-2 mt-1">
<x-text-input id="captcha_text" class="block " type="text" name="captcha_text"/>
<button type="button" class="inline-flex items-center ml-2 px-2 -pt-1 bg-rose-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-rose-700 active:bg-rose-900 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150" id="submitcaptcha" >
Submit
</button>
</div> </div>
<br> <br>
<p class="text-gray-800 dark:text-gray-200 text-sm"> <p class="text-gray-800 dark:text-gray-200 text-sm">
@@ -51,21 +40,12 @@
<script> <script>
var downloadCounter = 0; var downloadCounter = 0;
function reloadCaptcha() {
window.axios.get('/reload-captcha').then(function (response) {
if (response.status == 200) {
document.querySelector("#captchaImg").innerHTML = response.data.captcha;
}
}).catch(function (error) {
console.log(error);
});
}
function submitCaptcha() { function submitCaptcha(captchaToken) {
document.querySelector("#message").innerHTML = ''; document.querySelector("#message").innerHTML = '';
window.axios.post('/get-download', { window.axios.post('/get-download', {
captcha: document.getElementById('captcha_text').value, episode_id: document.getElementById('e_id').value,
episode_id: document.getElementById('e_id').value captcha: captchaToken,
}).then(function (response) { }).then(function (response) {
document.querySelector("#captcharequired").style.display = "none"; document.querySelector("#captcharequired").style.display = "none";
document.querySelector("#captchsolved").style.display = "block"; document.querySelector("#captchsolved").style.display = "block";
@@ -89,6 +69,16 @@
document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter); document.querySelector("#downloadEpisode").addEventListener("click", increaseDownloadCounter);
document.querySelector("#reloadcaptcha").addEventListener("click", reloadCaptcha); document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#submitcaptcha").addEventListener("click", submitCaptcha); const altcha = document.querySelector("#altcha");
altcha.addEventListener("statechange", (ev) => {
if (ev.detail.state === "verified") {
submitCaptcha(ev.detail.payload);
// Remove captcha from DOM
altcha.remove();
}
});
});
</script> </script>

View File

@@ -6,10 +6,10 @@
<div class="flex-grow"> <div class="flex-grow">
<div class="flex gap-2"> <div class="flex gap-2">
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p> <p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@if($comment->user->is_admin) @if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a> <a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif @endif
@if($comment->user->is_patreon) @if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a> <a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
@endif @endif
</div> </div>

View File

@@ -46,7 +46,7 @@
@include('modals.share') @include('modals.share')
@auth @auth
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
@include('admin.modals.upload-episode') @include('admin.modals.upload-episode')
@include('admin.modals.add-subtitles') @include('admin.modals.add-subtitles')
@include('admin.modals.edit-episode') @include('admin.modals.edit-episode')

View File

@@ -4,7 +4,7 @@
<div class="flex flex-col py-5 pl-24"> <div class="flex flex-col py-5 pl-24">
<strong class="text-slate-900 text-xl font-bold dark:text-slate-200"> <strong class="text-slate-900 text-xl font-bold dark:text-slate-200">
{{ $user->name }} {{ $user->name }}
@if ($user->is_patreon) @if ($user->hasRole(\App\Enums\UserRole::SUPPORTER))
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i <a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i
class="fa-solid fa-hand-holding-heart text-rose-600 animate-pulse"></i></a> class="fa-solid fa-hand-holding-heart text-rose-600 animate-pulse"></i></a>
@endif @endif

62
routes/admin.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
use App\Http\Controllers\Admin\AlertController;
use App\Http\Controllers\Admin\ContactController;
use App\Http\Controllers\Admin\CommentsController;
use App\Http\Controllers\Admin\EpisodeController;
use App\Http\Controllers\Admin\ReleaseController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\SubtitleController;
use App\Http\Controllers\Admin\SiteBackgroundController;
use App\Http\Controllers\Api\AdminApiController;
use Illuminate\Support\Facades\Route;
/*
|---------------------------------------------------------------------------------
| Admin Routes
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'auth.admin']], function () {
// Site alerts
Route::get('/admin/alert', [AlertController::class, 'index'])->name('admin.alert.index');
Route::post('/admin/alert', [AlertController::class, 'store'])->name('admin.alert.create');
Route::delete('/admin/alert/{alert_id}', [AlertController::class, 'delete'])->name('admin.alert.delete');
// Users
Route::get('/admin/users', [UserController::class, 'index'])->name('admin.user.index');
Route::post('/admin/users', [UserController::class, 'update'])->name('admin.user.update');
// Comments
Route::get('/admin/comments', [CommentsController::class, 'index'])->name('admin.comments.index');
// Contact page overview
Route::get('/admin/contact', [ContactController::class, 'index'])->name('admin.contact.index');
Route::delete('/admin/contact/{contact_id}', [ContactController::class, 'delete'])->name('admin.contact.delete');
// Site background settings
Route::get('/admin/background', [SiteBackgroundController::class, 'index'])->name('admin.background.index');
Route::post('/admin/background', [SiteBackgroundController::class, 'create'])->name('admin.background.create');
Route::put('/admin/background', [SiteBackgroundController::class, 'update'])->name('admin.background.update');
Route::delete('/admin/background', [SiteBackgroundController::class, 'delete'])->name('admin.background.delete');
// Release
Route::get('/admin/release', [ReleaseController::class, 'index'])->name('admin.upload.index');
Route::post('/admin/release/upload', [ReleaseController::class, 'store'])->name('admin.upload');
// Episode
Route::post('/admin/episode/upload', [EpisodeController::class, 'store'])->name('admin.upload.episode');
Route::post('/admin/episode/edit', [EpisodeController::class, 'update'])->name('admin.edit');
// Get Tags used for Upload Form
Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags');
Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios');
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
// Subtitles
Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles');
Route::post('/admin/add-new-subtitle', [SubtitleController::class, 'store'])->name('admin.add.new.subtitle');
Route::post('/admin/update-subtitles', [SubtitleController::class, 'update'])->name('admin.update.subtitles');
});

45
routes/user.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\PlaylistController;
use App\Http\Controllers\Api\UserApiController;
use Illuminate\Support\Facades\Route;
/*
|---------------------------------------------------------------------------------
| User Routes
|---------------------------------------------------------------------------------
*/
Route::middleware('auth')->group(function () {
Route::get('/user/profile', [ProfileController::class, 'index'])->name('profile.show');
Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments');
Route::get('/user/likes', [ProfileController::class, 'likes'])->name('profile.likes');
Route::get('/user/watched', [ProfileController::class, 'watched'])->name('user.watched');
// Notifications
Route::get('/user/notifications', [App\Http\Controllers\NotificationController::class, 'index'])->name('profile.notifications');
Route::delete('/user/notifications', [App\Http\Controllers\NotificationController::class, 'delete'])->name('profile.notifications.delete');
// User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete');
Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save');
Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist');
Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save');
// Playlist Routes for User Page
Route::get('/user/playlists', [PlaylistController::class, 'playlists'])->name('profile.playlists');
Route::get('/user/playlist/{playlist_id}', [PlaylistController::class, 'showPlaylist'])->name('profile.playlist.show');
Route::post('/create-playlist', [PlaylistController::class, 'createPlaylist'])->name('profile.playlists.create');
Route::delete('/user/playlist/{playlist_id}', [PlaylistController::class, 'deletePlaylist'])->name('profile.playlist.delete');
Route::post('/user/playlist-episode', [PlaylistController::class, 'deleteEpisodeFromPlaylist'])->name('playlist.delete.episode');
// Playlist Routes for Modals on Stream Page
Route::post('/hentai/add-to-playlist', [PlaylistController::class, 'addPlaylistApi'])->name('hentai.playlists.add');
Route::post('/hentai/create-playlist', [PlaylistController::class, 'createPlaylistApi'])->name('hentai.playlists.create');
// Download Page
Route::get('/download-search', [HomeController::class, 'downloadSearch'])->name('download.search');
});

View File

@@ -3,14 +3,12 @@
use App\Http\Controllers\ContactController; use App\Http\Controllers\ContactController;
use App\Http\Controllers\HomeController; use App\Http\Controllers\HomeController;
use App\Http\Controllers\PlaylistController; use App\Http\Controllers\PlaylistController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\StreamController; use App\Http\Controllers\StreamController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\Api\AdminApiController;
use App\Http\Controllers\Api\DownloadApiController; use App\Http\Controllers\Api\DownloadApiController;
use App\Http\Controllers\Api\HentaiApiController; use App\Http\Controllers\Api\HentaiApiController;
use App\Http\Controllers\Api\StreamApiController; use App\Http\Controllers\Api\StreamApiController;
use App\Http\Controllers\Api\UserApiController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@@ -40,100 +38,15 @@ Route::post('/search', [HomeController::class, 'searchRedirect'])->name('hentai.
Route::get('/contact', [ContactController::class, 'index'])->name('contact.index'); Route::get('/contact', [ContactController::class, 'index'])->name('contact.index');
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store'); Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');
// Public Playlistts // Public Playlists
Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlist.index'); Route::get('/playlists', [PlaylistController::class, 'index'])->name('playlist.index');
Route::get('/playlist/{playlist_id}', [PlaylistController::class, 'show'])->name('playlist.show'); Route::get('/playlist/{playlist_id}', [PlaylistController::class, 'show'])->name('playlist.show');
// Captcha Reload
Route::get('/reload-captcha', [ContactController::class, 'reloadCaptcha']);
// Download // Download
Route::post('/get-download', [DownloadApiController::class, 'getDownload']); Route::post('/get-download', [DownloadApiController::class, 'getDownload']);
Route::post('/update-language', [HomeController::class, 'updateLanguage'])->name('update.language'); Route::post('/update-language', [HomeController::class, 'updateLanguage'])->name('update.language');
// User Routes require __DIR__.'/user.php';
Route::middleware('auth')->group(function () { require __DIR__.'/admin.php';
Route::get('/user/profile', [ProfileController::class, 'index'])->name('profile.show');
Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments');
Route::get('/user/likes', [ProfileController::class, 'likes'])->name('profile.likes');
Route::get('/user/watched', [ProfileController::class, 'watched'])->name('user.watched');
// Notifications
Route::get('/user/notifications', [App\Http\Controllers\NotificationController::class, 'index'])->name('profile.notifications');
Route::delete('/user/notifications', [App\Http\Controllers\NotificationController::class, 'delete'])->name('profile.notifications.delete');
// User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::patch('/user/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/user/delete', [ProfileController::class, 'destroy'])->name('profile.delete');
Route::post('/user/settings', [ProfileController::class, 'saveSettings'])->name('profile.settings.save');
Route::get('/user/blacklist', [UserApiController::class, 'getBlacklist'])->name('profile.blacklist');
Route::post('/user/blacklist', [ProfileController::class, 'saveBlacklist'])->name('profile.blacklist.save');
// Playlist Routes for User Page
Route::get('/user/playlists', [PlaylistController::class, 'playlists'])->name('profile.playlists');
Route::get('/user/playlist/{playlist_id}', [PlaylistController::class, 'showPlaylist'])->name('profile.playlist.show');
Route::post('/create-playlist', [PlaylistController::class, 'createPlaylist'])->name('profile.playlists.create');
Route::delete('/user/playlist/{playlist_id}', [PlaylistController::class, 'deletePlaylist'])->name('profile.playlist.delete');
Route::post('/user/playlist-episode', [PlaylistController::class, 'deleteEpisodeFromPlaylist'])->name('playlist.delete.episode');
// Playlist Routes for Modals on Stream Page
Route::post('/hentai/add-to-playlist', [PlaylistController::class, 'addPlaylistApi'])->name('hentai.playlists.add');
Route::post('/hentai/create-playlist', [PlaylistController::class, 'createPlaylistApi'])->name('hentai.playlists.create');
// Download Page
Route::get('/download-search', [HomeController::class, 'downloadSearch'])->name('download.search');
});
/*
|---------------------------------------------------------------------------------
| Admin Pages
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'auth.admin']], function () {
// Site alerts
Route::get('/admin/alert', [App\Http\Controllers\Admin\AlertController::class, 'index'])->name('admin.alert.index');
Route::post('/admin/alert', [App\Http\Controllers\Admin\AlertController::class, 'store'])->name('admin.alert.create');
Route::delete('/admin/alert/{alert_id}', [App\Http\Controllers\Admin\AlertController::class, 'delete'])->name('admin.alert.delete');
// Users
Route::get('/admin/users', [App\Http\Controllers\Admin\UserController::class, 'index'])->name('admin.user.index');
Route::post('/admin/users', [App\Http\Controllers\Admin\UserController::class, 'update'])->name('admin.user.update');
// Comments
Route::get('/admin/comments', [App\Http\Controllers\Admin\CommentsController::class, 'index'])->name('admin.comments.index');
// Contact page overview
Route::get('/admin/contact', [App\Http\Controllers\Admin\ContactController::class, 'index'])->name('admin.contact.index');
Route::delete('/admin/contact/{contact_id}', [App\Http\Controllers\Admin\ContactController::class, 'delete'])->name('admin.contact.delete');
// Site background settings
Route::get('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'index'])->name('admin.background.index');
Route::post('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'create'])->name('admin.background.create');
Route::put('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'update'])->name('admin.background.update');
Route::delete('/admin/background', [App\Http\Controllers\Admin\SiteBackgroundController::class, 'delete'])->name('admin.background.delete');
// Release
Route::get('/admin/release', [App\Http\Controllers\Admin\ReleaseController::class, 'index'])->name('admin.upload.index');
Route::post('/admin/release/upload', [App\Http\Controllers\Admin\ReleaseController::class, 'store'])->name('admin.upload');
// Episode
Route::post('/admin/episode/upload', [App\Http\Controllers\Admin\EpisodeController::class, 'store'])->name('admin.upload.episode');
Route::post('/admin/episode/edit', [App\Http\Controllers\Admin\EpisodeController::class, 'update'])->name('admin.edit');
// Get Tags used for Upload Form
Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags');
Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios');
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
// Subtitles
Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles');
Route::post('/admin/add-new-subtitle', [App\Http\Controllers\Admin\SubtitleController::class, 'store'])->name('admin.add.new.subtitle');
Route::post('/admin/update-subtitles', [App\Http\Controllers\Admin\SubtitleController::class, 'update'])->name('admin.update.subtitles');
});
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';