Add Passkey Support & Pint

This commit is contained in:
2026-04-21 15:56:46 +02:00
parent 8ae9eaaadb
commit 05d4ef1bdb
57 changed files with 2151 additions and 716 deletions

View File

@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Alert;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AlertController extends Controller
{
/**
* Display alert index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.alert.index');
}
@@ -19,7 +21,7 @@ class AlertController extends Controller
/**
* Create Alert.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'message' => 'required|string|max:255',
@@ -39,7 +41,7 @@ class AlertController extends Controller
/**
* Delete Alert.
*/
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
public function delete(int $alert_id): RedirectResponse
{
Alert::where('id', $alert_id)->delete();

View File

@@ -3,13 +3,14 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class CommentsController extends Controller
{
/**
* Display Comments Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.comments.index');
}

View File

@@ -4,13 +4,15 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Contact;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$contacts = Contact::orderBy('created_at', 'DESC')->get();
@@ -22,7 +24,7 @@ class ContactController extends Controller
/**
* Delete Contact.
*/
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse
public function delete(int $contact_id): RedirectResponse
{
Contact::where('id', $contact_id)->delete();

View File

@@ -8,6 +8,7 @@ use App\Models\Episode;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EpisodeController extends Controller
@@ -31,7 +32,7 @@ class EpisodeController extends Controller
/**
* Add Episode to existing series
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
@@ -59,7 +60,7 @@ class EpisodeController extends Controller
/**
* Edit Episode
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);

View File

@@ -8,7 +8,9 @@ use App\Models\Hentai;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ReleaseController extends Controller
{
@@ -31,7 +33,7 @@ class ReleaseController extends Controller
/**
* Display release page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.release.create');
}
@@ -39,7 +41,7 @@ class ReleaseController extends Controller
/**
* Upload New Hentai with One or Multipe Episodes
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
// Create new Hentai or find existing one
$slug = $this->episodeService->generateSlug($request->input('title'));

View File

@@ -4,10 +4,12 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\SiteBackground;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
@@ -16,7 +18,7 @@ class SiteBackgroundController extends Controller
/**
* Display admin index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.background.index', [
'images' => SiteBackground::all(),
@@ -26,7 +28,7 @@ class SiteBackgroundController extends Controller
/**
* Create new site backgrounds
*/
public function create(Request $request): \Illuminate\Http\RedirectResponse
public function create(Request $request): RedirectResponse
{
$request->validate([
'images' => 'required',
@@ -73,7 +75,7 @@ class SiteBackgroundController extends Controller
return redirect()->back();
}
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$request->validate([
'id' => 'required|exists:site_backgrounds,id',
@@ -96,7 +98,7 @@ class SiteBackgroundController extends Controller
/**
* Delete backround
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
$id = $request->input('id');

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\EpisodeSubtitle;
use App\Models\Subtitle;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class SubtitleController extends Controller
@@ -13,7 +14,7 @@ class SubtitleController extends Controller
/**
* Add new Subtitle.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$subtitle = Subtitle::create([
'name' => $request->name,
@@ -32,7 +33,7 @@ class SubtitleController extends Controller
/**
* Update Episode Subtitles.
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();

View File

@@ -6,13 +6,14 @@ use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Display Users Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.users.index');
}

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
@@ -26,7 +27,7 @@ class NewPasswordController extends Controller
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{

View File

@@ -11,13 +11,14 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
class RegisteredUserController extends Controller
{
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{

View File

@@ -4,14 +4,16 @@ namespace App\Http\Controllers;
use App\Models\Contact;
use App\Rules\ValidCaptcha;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('contact.form');
}
@@ -19,7 +21,7 @@ class ContactController extends Controller
/**
* Store Contact Submission.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',

View File

@@ -4,15 +4,17 @@ namespace App\Http\Controllers;
use App\Helpers\CacheHelper;
use App\Models\Episode;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class HomeController extends Controller
{
/**
* Display Home Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$guest = Auth::guest();
@@ -45,7 +47,7 @@ class HomeController extends Controller
/**
* Display Banned Page.
*/
public function banned(): \Illuminate\View\View
public function banned(): View
{
return view('auth.banned');
}
@@ -54,7 +56,7 @@ class HomeController extends Controller
* Redirects to a random Hentai episode
* Done due to performance reasons
*/
public function random(): \Illuminate\Http\RedirectResponse
public function random(): RedirectResponse
{
$random = Episode::inRandomOrder()
->limit(1)
@@ -69,7 +71,7 @@ class HomeController extends Controller
/**
* Display Search Page.
*/
public function search(): \Illuminate\View\View
public function search(): View
{
return view('search.index');
}
@@ -77,7 +79,7 @@ class HomeController extends Controller
/**
* Display Download Search Page.
*/
public function downloadSearch(): \Illuminate\View\View
public function downloadSearch(): View
{
return view('search.download');
}
@@ -85,7 +87,7 @@ class HomeController extends Controller
/**
* Redirect POST Data to GET with Query String.
*/
public function searchRedirect(Request $request): \Illuminate\Http\RedirectResponse
public function searchRedirect(Request $request): RedirectResponse
{
return redirect()->route('hentai.search', [
'search' => $request->input('live-search'),
@@ -95,7 +97,7 @@ class HomeController extends Controller
/**
* Display Stats Page.
*/
public function stats(): \Illuminate\View\View
public function stats(): View
{
return view('home.stats', [
'viewCount' => CacheHelper::getTotalViewCount(),
@@ -107,7 +109,7 @@ class HomeController extends Controller
/**
* Manually set website language
*/
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
public function updateLanguage(Request $request): RedirectResponse
{
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);

View File

@@ -5,13 +5,14 @@ namespace App\Http\Controllers;
use App\Http\Requests\MatrixRegisterRequest;
use App\Services\MatrixRegistrationService;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MatrixController extends Controller
{
/**
* Display the user page.
*/
public function index(Request $request): \Illuminate\View\View
public function index(Request $request): View
{
$rooms = [
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],

View File

@@ -2,14 +2,16 @@
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class NotificationController extends Controller
{
/**
* Display the user's notification page.
*/
public function index(Request $request): \Illuminate\View\View
public function index(Request $request): View
{
return view('profile.notifications', [
'user' => $request->user(),
@@ -20,7 +22,7 @@ class NotificationController extends Controller
/**
* Delete Notifcation
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
$request->validate([
'id' => 'required|exists:notifications,id',

View File

@@ -6,7 +6,10 @@ use App\Models\Episode;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Services\PlaylistService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PlaylistController extends Controller
{
@@ -20,7 +23,7 @@ class PlaylistController extends Controller
/**
* Display the public playlists page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('playlist.index');
}
@@ -28,7 +31,7 @@ class PlaylistController extends Controller
/**
* Display public playlist.
*/
public function show($playlist_id): \Illuminate\View\View
public function show($playlist_id): View
{
if (! is_numeric($playlist_id)) {
abort(404);
@@ -44,7 +47,7 @@ class PlaylistController extends Controller
/**
* Display the user's playlists page.
*/
public function playlists(Request $request): \Illuminate\View\View
public function playlists(Request $request): View
{
$title = 'Delete Playlist!';
$text = 'Are you sure you want to delete?';
@@ -59,7 +62,7 @@ class PlaylistController extends Controller
/**
* Display user's playlist.
*/
public function showPlaylist(Request $request, $playlist_id): \Illuminate\View\View
public function showPlaylist(Request $request, $playlist_id): View
{
if (! is_numeric($playlist_id)) {
abort(404);
@@ -77,7 +80,7 @@ class PlaylistController extends Controller
/**
* Create user playlist (Form).
*/
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse
public function createPlaylist(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
@@ -95,7 +98,7 @@ class PlaylistController extends Controller
/**
* Delete user playlist.
*/
public function deletePlaylist(Request $request, $playlist_id): \Illuminate\Http\RedirectResponse
public function deletePlaylist(Request $request, $playlist_id): RedirectResponse
{
if (! is_numeric($playlist_id)) {
abort(404);
@@ -115,7 +118,7 @@ class PlaylistController extends Controller
/**
* Delete episode from playlist.
*/
public function deleteEpisodeFromPlaylist(Request $request): \Illuminate\Http\JsonResponse
public function deleteEpisodeFromPlaylist(Request $request): JsonResponse
{
if (! is_numeric($request->input('playlist')) || ! is_numeric($request->input('episode'))) {
return response()->json([
@@ -143,7 +146,7 @@ class PlaylistController extends Controller
/**
* Add to user playlist (API).
*/
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function addPlaylistApi(Request $request): JsonResponse
{
$user = $request->user();
@@ -180,7 +183,7 @@ class PlaylistController extends Controller
/**
* Create user playlist (API).
*/
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function createPlaylistApi(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|max:30',

View File

@@ -8,6 +8,7 @@ use App\Models\User;
use Conner\Tagging\Model\Tag;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redirect;
@@ -138,7 +139,7 @@ class ProfileController extends Controller
/**
* Delete the user's account.
*/
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
public function destroy(Request $request): RedirectResponse
{
$user = $request->user();
@@ -173,7 +174,7 @@ class ProfileController extends Controller
/**
* Store custom user avatar.
*/
protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void
protected function storeAvatar(UploadedFile $file, User $user): void
{
// Create Folder for Image Upload
if (! Storage::disk('public')->exists('/images/avatars')) {

View File

@@ -13,13 +13,14 @@ use hisorange\BrowserDetect\Facade as Browser;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class StreamController extends Controller
{
/**
* Display Stream Page.
*/
public function index(Request $request, string $title): \Illuminate\View\View
public function index(Request $request, string $title): View
{
$titleParts = explode('-', $title);
if (! is_numeric($titleParts[array_key_last($titleParts)])) {

View File

@@ -2,7 +2,34 @@
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsBanned;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SetLocale;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
TrustProxies::class,
HandleCors::class,
PreventRequestsDuringMaintenance::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
/**
@@ -30,20 +57,20 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\IsBanned::class,
\App\Http\Middleware\SetLocale::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
IsBanned::class,
SetLocale::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
ThrottleRequests::class.':api',
SubstituteBindings::class,
],
];
@@ -55,18 +82,18 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => AuthenticateSession::class,
'auth.admin' => IsAdmin::class,
'auth.moderator' => IsModerator::class,
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'password.confirm' => RequirePassword::class,
'precognitive' => HandlePrecognitiveRequests::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
];
}

View File

@@ -13,7 +13,7 @@ class IsModerator
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{

View File

@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{

View File

@@ -13,7 +13,7 @@ class SetLocale
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{

View File

@@ -4,6 +4,7 @@ namespace App\Http\Requests\Auth;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
@@ -23,7 +24,7 @@ class LoginRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
@@ -37,7 +38,7 @@ class LoginRequest extends FormRequest
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function authenticate(): void
{
@@ -57,7 +58,7 @@ class LoginRequest extends FormRequest
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class MatrixRegisterRequest extends FormRequest
@@ -20,7 +21,7 @@ class MatrixRegisterRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{

View File

@@ -3,6 +3,7 @@
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{

View File

@@ -2,6 +2,7 @@
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Downloads;
use Livewire\Attributes\Url;
use Livewire\Component;
@@ -74,9 +75,9 @@ class DownloadsSearch extends Component
$types[] = 'FHD';
} elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHDi';
}
}
@@ -99,7 +100,7 @@ class DownloadsSearch extends Component
public function mount()
{
if (! auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
return;
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
class SiteBackground extends Model
{
@@ -21,7 +22,7 @@ class SiteBackground extends Model
/**
* Returns the current IDs of active wallpaper
*/
public function getImages(): ?\Illuminate\Support\Collection
public function getImages(): ?Collection
{
$now = Carbon::now();

View File

@@ -11,10 +11,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys;
class User extends Authenticatable
class User extends Authenticatable implements HasPasskeys
{
use HasFactory, Notifiable;
use HasFactory, InteractsWithPasskeys, Notifiable;
/**
* The attributes that are mass assignable.

View File

@@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use SocialiteProviders\Discord\Provider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class AppServiceProvider extends ServiceProvider
{
@@ -20,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite('discord', Provider::class);
});
}
}

View File

@@ -11,6 +11,7 @@ use AltchaOrg\Altcha\Solution;
use AltchaOrg\Altcha\VerifySolutionOptions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;
/**
* Validation rule to verify captcha solution.
@@ -97,7 +98,7 @@ class ValidCaptcha implements ValidationRule
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{

View File

@@ -1,5 +1,10 @@
<?php
use App\Exceptions\Handler;
use App\Http\Kernel;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application;
/*
|--------------------------------------------------------------------------
| Create The Application
@@ -11,7 +16,7 @@
|
*/
$app = new Illuminate\Foundation\Application(
$app = new Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
Kernel::class
);
$app->singleton(
@@ -37,8 +42,8 @@ $app->singleton(
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
ExceptionHandler::class,
Handler::class
);
/*

View File

@@ -28,6 +28,7 @@
"rtconner/laravel-tagging": "^5.0",
"socialiteproviders/discord": "^4.2",
"spatie/laravel-discord-alerts": "^1.8",
"spatie/laravel-passkeys": "^1.7",
"spatie/laravel-sitemap": "^7.3"
},
"require-dev": {

2325
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,12 @@
<?php
use App\Providers\AppServiceProvider;
use App\Providers\AuthServiceProvider;
use App\Providers\EventServiceProvider;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
use Mews\Captcha\Facades\Captcha;
return [
@@ -175,11 +180,11 @@ return [
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
AppServiceProvider::class,
AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
EventServiceProvider::class,
RouteServiceProvider::class,
])->toArray(),
/*
@@ -195,7 +200,7 @@ return [
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
'Captcha' => Mews\Captcha\Facades\Captcha::class,
'Captcha' => Captcha::class,
])->toArray(),
];

View File

@@ -1,5 +1,7 @@
<?php
use App\Models\User;
return [
/*
@@ -62,7 +64,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
'model' => User::class,
],
// 'users' => [

View File

@@ -1,5 +1,7 @@
<?php
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;
return [
/*
* The webhook URLs that we'll use to send a message to Discord.
@@ -14,5 +16,5 @@ return [
* This job will send the message to Discord. You can extend this
* job to set timeouts, retries, etc...
*/
'job' => Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob::class,
'job' => SendToDiscordChannelJob::class,
];

View File

@@ -1,5 +1,7 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
@@ -16,7 +18,7 @@ return [
|
*/
'driver' => \Intervention\Image\Drivers\Gd\Driver::class,
'driver' => Driver::class,
/*
|--------------------------------------------------------------------------

49
config/passkeys.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
use App\Models\User;
use Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction;
use Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction;
use Spatie\LaravelPasskeys\Actions\StorePasskeyAction;
use Spatie\LaravelPasskeys\Models\Passkey;
return [
/*
* After a successful authentication attempt using a passkey
* we'll redirect to this URL.
*/
'redirect_to_after_login' => '/',
/*
* These class are responsible for performing core tasks regarding passkeys.
* You can customize them by creating a class that extends the default, and
* by specifying your custom class name here.
*/
'actions' => [
'generate_passkey_register_options' => GeneratePasskeyRegisterOptionsAction::class,
'store_passkey' => StorePasskeyAction::class,
'generate_passkey_authentication_options' => GeneratePasskeyAuthenticationOptionsAction::class,
'find_passkey' => FindPasskeyToAuthenticateAction::class,
'configure_ceremony_step_manager_factory' => ConfigureCeremonyStepManagerFactoryAction::class,
],
/*
* These properties will be used to generate the passkey.
*/
'relying_party' => [
'name' => config('app.name'),
'id' => parse_url(config('app.url'), PHP_URL_HOST),
'icon' => null,
],
/*
* The models used by the package.
*
* You can override this by specifying your own models
*/
'models' => [
'passkey' => Passkey::class,
'authenticatable' => env('AUTH_MODEL', User::class),
],
];

View File

@@ -1,5 +1,8 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
@@ -60,9 +63,9 @@ return [
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];

View File

@@ -2,10 +2,11 @@
namespace Database\Factories;
use App\Models\Episode;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Episode>
* @extends Factory<Episode>
*/
class EpisodeFactory extends Factory
{

View File

@@ -2,10 +2,11 @@
namespace Database\Factories;
use App\Models\Hentai;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Hentai>
* @extends Factory<Hentai>
*/
class HentaiFactory extends Factory
{

View File

@@ -2,10 +2,11 @@
namespace Database\Factories;
use App\Models\Studios;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Studios>
* @extends Factory<Studios>
*/
class StudiosFactory extends Factory
{

View File

@@ -2,11 +2,12 @@
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
* @extends Factory<User>
*/
class UserFactory extends Factory
{

View File

@@ -50,7 +50,7 @@ return new class extends Migration
$alreadyexists = Episode::where('slug', $episode->slug)->first();
if ($alreadyexists) {
throw new \RuntimeException('Migration stopped! Slug already exists: '.$episode->slug);
throw new RuntimeException('Migration stopped! Slug already exists: '.$episode->slug);
}
$episode->save();

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Spatie\LaravelPasskeys\Support\Config;
return new class extends Migration
{
public function up()
{
$authenticatableClass = Config::getAuthenticatableModel();
$authenticatableTableName = (new $authenticatableClass)->getTable();
Schema::create('passkeys', function (Blueprint $table) use ($authenticatableTableName, $authenticatableClass) {
$table->id();
$table
->foreignIdFor($authenticatableClass, 'authenticatable_id')
->constrained(table: $authenticatableTableName, indexName: 'passkeys_authenticatable_fk')
->cascadeOnDelete();
$table->text('name');
$table->text('credential_id');
$table->json('data');
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
};

7
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@simplewebauthn/browser": "^13.3.0",
"@yaireo/tagify": "^4.21.2",
"altcha": "^3.0.0",
"chart.js": "^4.5.0",
@@ -998,6 +999,12 @@
"win32"
]
},
"node_modules/@simplewebauthn/browser": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz",
"integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==",
"license": "MIT"
},
"node_modules/@svta/cml-608": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.1.tgz",

View File

@@ -19,6 +19,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@simplewebauthn/browser": "^13.3.0",
"@yaireo/tagify": "^4.21.2",
"altcha": "^3.0.0",
"chart.js": "^4.5.0",

View File

@@ -23,3 +23,16 @@ import "altcha/themes/cupcake.css";
// Alpine.start();
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
/**
* Passkey Support
*/
import {
browserSupportsWebAuthn,
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser'
window.browserSupportsWebAuthn = browserSupportsWebAuthn;
window.startAuthentication = startAuthentication;
window.startRegistration = startRegistration;

View File

@@ -31,7 +31,18 @@
<!-- Or -->
<div class="grid grid-cols-3">
<hr class="self-center border-neutral-600">
<p>OR</p>
<p class="text-neutral-800 dark:text-neutral-400">OR</p>
<hr class="self-center border-neutral-600">
</div>
</div>
<!-- Passkey Login -->
<div class="w-full text-center text-white mb-3">
<x-authenticate-passkey />
<div class="grid grid-cols-3 pt-3">
<hr class="self-center border-neutral-600">
<p class="text-neutral-800 dark:text-neutral-400">OR</p>
<hr class="self-center border-neutral-600">
</div>
</div>

View File

@@ -15,6 +15,9 @@
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.update-profile-information-form')
</div>
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
<livewire:passkeys />
</div>
<div class="p-4 sm:p-8 bg-white/40 dark:bg-neutral-950/40 backdrop-blur shadow sm:rounded-lg">
@include('profile.partials.update-password-form')
</div>

View File

@@ -0,0 +1,23 @@
<div>
@include('passkeys::components.partials.authenticateScript')
<form id="passkey-login-form" method="POST" action="{{ route('passkeys.login') }}">
@csrf
</form>
@if($message = session()->get('authenticatePasskey::message'))
<div class="bg-red-100 text-red-700 p-4 border border-red-400 rounded">
{{ $message }}
</div>
@endif
<div onclick="authenticateWithPasskey()">
@if ($slot->isEmpty())
<div class="bg-zinc-700 hover:bg-zinc-600 text-white font-bold px-4 h-10 rounded text-center p-[10px] cursor-pointer">
<i class="fa-solid fa-key"></i> {{ __('passkeys::passkeys.authenticate_using_passkey') }}
</div>
@else
{{ $slot }}
@endif
</div>
</div>

View File

@@ -0,0 +1,18 @@
<script>
async function authenticateWithPasskey(remember = false) {
const response = await fetch('{{ route('passkeys.authentication_options') }}')
const options = await response.json();
const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, });
const form = document.getElementById('passkey-login-form');
form.addEventListener('formdata', ({formData}) => {
formData.set('remember', remember);
formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
});
form.submit();
}
</script>

View File

@@ -0,0 +1,11 @@
@script
<script>
Livewire.on('passkeyPropertiesValidated', async function (eventData) {
const passkeyOptions = eventData[0].passkeyOptions;
const passkey = await startRegistration({ optionsJSON: passkeyOptions });
@this.call('storePasskey', JSON.stringify(passkey));
});
</script>
@endscript

View File

@@ -0,0 +1,44 @@
<section>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('passkeys::passkeys.passkeys') }}
</h2>
<div class="mt-2">
<label for="name" class="block mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('passkeys::passkeys.name') }}
</label>
<form id="passkeyForm" wire:submit="validatePasskeyProperties" class="flex items-center space-x-2 mt-2">
<div>
<input autocomplete="off" type="text" wire:model="name" class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm">
@error('name')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 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">
{{ __('passkeys::passkeys.create') }}
</button>
</form>
</div>
<div class="mt-6">
<ul class="space-y-4">
@foreach($passkeys as $passkey)
<li class="flex justify-between items-center p-4 bg-white border-neutral-200 dark:bg-neutral-900 dark:border-neutral-700 border rounded-lg shadow-sm">
<div class="text-gray-900 dark:text-gray-100">
{{ $passkey->name }}
</div>
<div class="ml-2 text-gray-900 dark:text-gray-100">
{{ __('passkeys::passkeys.last_used') }}: {{ $passkey->last_used_at?->diffForHumans() ?? __('passkeys::passkeys.not_used_yet') }}
</div>
<div>
<button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex items-center px-4 py-2 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">
{{ __('passkeys::passkeys.delete') }}
</button>
</div>
</li>
@endforeach
</ul>
</div>
</section>
@include('passkeys::livewire.partials.createScript')

View File

@@ -2,6 +2,8 @@
use App\Http\Controllers\Api\UserApiController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\MatrixController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\PlaylistController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
@@ -13,11 +15,11 @@ use Illuminate\Support\Facades\Route;
*/
// Matrix
Route::get('/join-matrix', [App\Http\Controllers\MatrixController::class, 'index'])->name('join.matrix');
Route::get('/join-matrix', [MatrixController::class, 'index'])->name('join.matrix');
Route::middleware('auth')->group(function () {
// Matrix
Route::post('/join-matrix', [App\Http\Controllers\MatrixController::class, 'store'])->name('join.matrix.create');
Route::post('/join-matrix', [MatrixController::class, 'store'])->name('join.matrix.create');
Route::get('/user/profile', [ProfileController::class, 'index'])->name('profile.show');
Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments');
@@ -25,8 +27,8 @@ Route::middleware('auth')->group(function () {
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');
Route::get('/user/notifications', [NotificationController::class, 'index'])->name('profile.notifications');
Route::delete('/user/notifications', [NotificationController::class, 'delete'])->name('profile.notifications.delete');
// User Profile Actions
Route::get('/user/settings', [ProfileController::class, 'settings'])->name('profile.settings');

View File

@@ -45,6 +45,11 @@ Route::post('/get-download', [DownloadApiController::class, 'getDownload']);
Route::post('/update-language', [HomeController::class, 'updateLanguage'])->name('update.language');
/**
* Passkey Routes
*/
Route::passkeys();
require __DIR__.'/user.php';
require __DIR__.'/admin.php';
require __DIR__.'/auth.php';

View File

@@ -8,6 +8,7 @@ use App\Models\Hentai;
use App\Models\Studios;
use App\Services\GalleryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
@@ -38,7 +39,7 @@ class GalleryServiceTest extends TestCase
$file = UploadedFile::fake()->image('test_image.jpg');
// Create a request with the fake file
$request = new \Illuminate\Http\Request;
$request = new Request;
$request->files->add(['episodegallery1' => [$file]]);
// Call the method to test