diff --git a/app/Console/Commands/AutoStats.php b/app/Console/Commands/AutoStats.php index 5cfba35..a166ab8 100644 --- a/app/Console/Commands/AutoStats.php +++ b/app/Console/Commands/AutoStats.php @@ -30,9 +30,9 @@ class AutoStats extends Command */ public function handle() { - PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->forceDelete(); - PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->forceDelete(); - PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->forceDelete(); + PopularDaily::where('created_at', '<=', Carbon::now()->subMinutes(1440))->delete(); + PopularWeekly::where('created_at', '<=', Carbon::now()->subMinutes(10080))->delete(); + PopularMonthly::where('created_at', '<=', Carbon::now()->subMinutes(43200))->delete(); $this->comment('Automated Purge Stats Complete'); } diff --git a/app/Console/Commands/ResetUserDownloads.php b/app/Console/Commands/ResetUserDownloads.php index 7516122..b60c826 100644 --- a/app/Console/Commands/ResetUserDownloads.php +++ b/app/Console/Commands/ResetUserDownloads.php @@ -35,6 +35,6 @@ class ResetUserDownloads extends Command // Clear old downloads which have expired UserDownload::where('created_at', '<=', Carbon::now()->subHour(6)) - ->forceDelete(); + ->delete(); } } diff --git a/app/Http/Controllers/Admin/AlertController.php b/app/Http/Controllers/Admin/AlertController.php index b2018cd..50f5bc9 100644 --- a/app/Http/Controllers/Admin/AlertController.php +++ b/app/Http/Controllers/Admin/AlertController.php @@ -41,7 +41,7 @@ class AlertController extends Controller */ public function delete(int $alert_id): \Illuminate\Http\RedirectResponse { - Alert::where('id', $alert_id)->forceDelete(); + Alert::where('id', $alert_id)->delete(); cache()->forget('alerts'); diff --git a/app/Http/Controllers/Admin/SiteBackgroundController.php b/app/Http/Controllers/Admin/SiteBackgroundController.php index 86ab94d..134de2a 100644 --- a/app/Http/Controllers/Admin/SiteBackgroundController.php +++ b/app/Http/Controllers/Admin/SiteBackgroundController.php @@ -105,7 +105,7 @@ class SiteBackgroundController extends Controller DB::beginTransaction(); $bg = SiteBackground::where('id', $id)->firstOrFail(); - $bg->forceDelete(); + $bg->delete(); $resolutions = [1440, 1080, 720, 640]; try { diff --git a/app/Http/Controllers/Admin/SubtitleController.php b/app/Http/Controllers/Admin/SubtitleController.php index 5f04705..86a57a9 100644 --- a/app/Http/Controllers/Admin/SubtitleController.php +++ b/app/Http/Controllers/Admin/SubtitleController.php @@ -38,7 +38,7 @@ class SubtitleController extends Controller // Clear everything foreach($episode->subtitles as $sub) { - $sub->forceDelete(); + $sub->delete(); } if (! $request->input('subtitles')) { diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 494a106..765dec6 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; -use App\Providers\RouteServiceProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -29,7 +28,7 @@ class AuthenticatedSessionController extends Controller $request->session()->regenerate(); - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('home.index', absolute: false)); } /** diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php index 523ddda..af1d849 100644 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -36,6 +35,6 @@ class ConfirmablePasswordController extends Controller $request->session()->put('auth.password_confirmed_at', time()); - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('home.index', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/DiscordAuthController.php b/app/Http/Controllers/Auth/DiscordAuthController.php new file mode 100644 index 0000000..482f429 --- /dev/null +++ b/app/Http/Controllers/Auth/DiscordAuthController.php @@ -0,0 +1,130 @@ +redirect(); + } + + /** + * Callback received from Discord + */ + public function callback(): RedirectResponse + { + $discordUser = Socialite::driver('discord')->user(); + + $user = User::where('discord_id', $discordUser->id)->first(); + + if (!$user) { + // link by email if it already exists + $user = User::where('email', $discordUser->email)->first(); + + if ($user) { + $user->update([ + 'discord_id' => $discordUser->id, + 'discord_avatar' => $discordUser->avatar, + ]); + } else { + // Create new user + $user = User::create([ + 'name' => $discordUser->name, + 'email' => $discordUser->email, + 'discord_id' => $discordUser->id, + 'discord_avatar' => $discordUser->avatar, + 'password' => null, + ]); + } + } + + $this->checkDiscordAvatar($discordUser, $user); + $this->checkDiscordRoles($user); + + Auth::login($user, true); + + return redirect()->route('home.index'); + } + + /** + * Check if discord avatar changed + */ + private function checkDiscordAvatar(\Laravel\Socialite\Contracts\User $socialiteUser, User $user): void + { + if ($socialiteUser->avatar != $user->discord_avatar) { + $user->update(['discord_avatar' => $socialiteUser->avatar]); + } + } + + /** + * Check Discord Roles if user is Patreon member + */ + private function checkDiscordRoles(User $user): void + { + // Should not ever happen + if (!$user->discord_id) { + return; + } + + $guildId = config('discord.guild_id'); + + $response = Http::withToken(config('discord.discord_bot_token'), 'Bot') + ->timeout(5) + ->get("https://discord.com/api/v10/guilds/{$guildId}/members/{$user->discord_id}"); + + // User is not in the guild + if ($response->status() === 404) { + $user->update([ + 'is_patreon' => false, + ]); + + return; + } + + // Something else failed + if ($response->failed()) { + Log::warning('Discord role check failed', [ + 'user_id' => $user->id, + 'discord_id' => $user->discord_id, + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return; + } + + $discordRoles = $response->json('roles', []); + $patreonRoles = config('discord.patreon_roles', []); + + $isPatreon = false; + foreach($patreonRoles as $patreonRole) + { + if (in_array($patreonRole, $discordRoles, true)) { + $isPatreon = true; + break; + } + } + + // Only update if something actually changed + if ($user->is_patreon !== $isPatreon) { + $user->update([ + 'is_patreon' => $isPatreon, + ]); + } + } + +} diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php index 96ba772..364650a 100644 --- a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -15,7 +14,7 @@ class EmailVerificationNotificationController extends Controller public function store(Request $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(RouteServiceProvider::HOME); + return redirect()->intended(route('home.index', absolute: false)); } $request->user()->sendEmailVerificationNotification(); diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php index 186eb97..9d543c2 100644 --- a/app/Http/Controllers/Auth/EmailVerificationPromptController.php +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -16,7 +15,7 @@ class EmailVerificationPromptController extends Controller public function __invoke(Request $request): RedirectResponse|View { return $request->user()->hasVerifiedEmail() - ? redirect()->intended(RouteServiceProvider::HOME) + ? redirect()->intended(route('home.index', absolute: false)) : view('auth.verify-email'); } } diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php index f1e2814..e8368bd 100644 --- a/app/Http/Controllers/Auth/NewPasswordController.php +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Models\User; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -40,7 +41,7 @@ class NewPasswordController extends Controller // database. Otherwise we will parse the error and return the response. $status = Password::reset( $request->only('email', 'password', 'password_confirmation', 'token'), - function ($user) use ($request) { + function (User $user) use ($request) { $user->forceFill([ 'password' => Hash::make($request->password), 'remember_token' => Str::random(60), @@ -56,6 +57,6 @@ class NewPasswordController extends Controller return $status == Password::PASSWORD_RESET ? redirect()->route('login')->with('status', __($status)) : back()->withInput($request->only('email')) - ->withErrors(['email' => __($status)]); + ->withErrors(['email' => __($status)]); } } diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php index 6916409..56112b8 100644 --- a/app/Http/Controllers/Auth/PasswordController.php +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -15,6 +15,20 @@ class PasswordController extends Controller */ public function update(Request $request): RedirectResponse { + // If user logged in with Discord and has not yet a password, allow to set password + if ($request->user()->discord_id && is_null($request->user()->password)) + { + $validated = $request->validateWithBag('updatePassword', [ + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('status', 'password-updated'); + } + $validated = $request->validateWithBag('updatePassword', [ 'current_password' => ['required', 'current_password'], 'password' => ['required', Password::defaults(), 'confirmed'], diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php index ce813a6..bf1ebfa 100644 --- a/app/Http/Controllers/Auth/PasswordResetLinkController.php +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -39,6 +39,6 @@ class PasswordResetLinkController extends Controller return $status == Password::RESET_LINK_SENT ? back()->with('status', __($status)) : back()->withInput($request->only('email')) - ->withErrors(['email' => __($status)]); + ->withErrors(['email' => __($status)]); } } diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 5313f35..88b67e1 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -4,25 +4,15 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\User; -use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules; -use Illuminate\View\View; class RegisteredUserController extends Controller { - /** - * Display the registration view. - */ - public function create(): View - { - return view('auth.register'); - } - /** * Handle an incoming registration request. * @@ -32,7 +22,7 @@ class RegisteredUserController extends Controller { $request->validate([ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); @@ -46,6 +36,6 @@ class RegisteredUserController extends Controller Auth::login($user); - return redirect(RouteServiceProvider::HOME); + return redirect(route('home.index', absolute: false)); } } diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php index ea87940..8adaecb 100644 --- a/app/Http/Controllers/Auth/VerifyEmailController.php +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Http\RedirectResponse; @@ -16,13 +15,13 @@ class VerifyEmailController extends Controller public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { - return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + return redirect()->intended(route('home.index', absolute: false).'?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } - return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); + return redirect()->intended(route('home.index', absolute: false).'?verified=1'); } } diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php index 5bd3bb9..78ed78a 100644 --- a/app/Http/Controllers/NotificationController.php +++ b/app/Http/Controllers/NotificationController.php @@ -32,7 +32,7 @@ class NotificationController extends Controller ->where('id', $request->input('id')) ->firstOrFail(); - $notification->forceDelete(); + $notification->delete(); return redirect()->back(); } diff --git a/app/Http/Controllers/PlaylistController.php b/app/Http/Controllers/PlaylistController.php index 941b552..7bc1916 100644 --- a/app/Http/Controllers/PlaylistController.php +++ b/app/Http/Controllers/PlaylistController.php @@ -105,13 +105,11 @@ class PlaylistController extends Controller $user = $request->user(); - $playlist = Playlist::where('user_id', $user->id)->where('id', $playlist_id)->firstOrFail(); + $playlist = Playlist::where('user_id', $user->id) + ->where('id', $playlist_id) + ->firstOrFail(); - // Delete Playlist Episodes - PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete(); - - // Delete Playlist - $playlist->forceDelete(); + $playlist->delete(); return to_route('profile.playlists'); } @@ -128,8 +126,14 @@ class PlaylistController extends Controller ], 404); } - $playlist = Playlist::where('user_id', $request->user()->id)->where('id', (int) $request->input('playlist'))->firstOrFail(); - PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', (int) $request->input('episode'))->forceDelete(); + $playlist = Playlist::where('user_id', $request->user()->id) + ->where('id', (int) $request->input('playlist')) + ->firstOrFail(); + + PlaylistEpisode::where('playlist_id', $playlist->id) + ->where('episode_id', (int) $request->input('episode')) + ->delete(); + $this->playlistService->reorderPositions($playlist); return response()->json([ diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 23c1930..4d77ea2 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -3,12 +3,20 @@ namespace App\Http\Controllers; use App\Models\Episode; +use App\Models\Playlist; +use App\Models\PlaylistEpisode; +use App\Models\User; use App\Http\Requests\ProfileUpdateRequest; - +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Storage; +use Illuminate\View\View; + +use Intervention\Image\Laravel\Facades\Image; use Conner\Tagging\Model\Tag; @@ -17,7 +25,7 @@ class ProfileController extends Controller /** * Display the user page. */ - public function index(Request $request): \Illuminate\View\View + public function index(Request $request): View { return view('profile.index', [ 'user' => $request->user(), @@ -27,7 +35,7 @@ class ProfileController extends Controller /** * Display the user's settings form. */ - public function settings(Request $request): \Illuminate\View\View + public function settings(Request $request): View { $example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first(); @@ -37,10 +45,33 @@ class ProfileController extends Controller ]); } + /** + * Update the user's profile information. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $user = $request->user(); + + // Fill everything except the image + $user->fill($request->safe()->except('image')); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + if ($request->hasFile('image')) { + $this->storeAvatar($request->file('image'), $user); + } + + $user->save(); + + return Redirect::route('profile.settings')->with('status', 'profile-updated'); + } + /** * Display the user's watched page. */ - public function watched(Request $request): \Illuminate\View\View + public function watched(Request $request): View { return view('profile.watched', [ 'user' => $request->user(), @@ -50,7 +81,7 @@ class ProfileController extends Controller /** * Display the user's comments page. */ - public function comments(Request $request): \Illuminate\View\View + public function comments(Request $request): View { return view('profile.comments', [ 'user' => $request->user(), @@ -60,7 +91,7 @@ class ProfileController extends Controller /** * Display the user's likes page. */ - public function likes(Request $request): \Illuminate\View\View + public function likes(Request $request): View { return view('profile.likes', [ 'user' => $request->user(), @@ -70,7 +101,7 @@ class ProfileController extends Controller /** * Update user settings. */ - public function saveSettings(Request $request): \Illuminate\Http\RedirectResponse + public function saveSettings(Request $request): RedirectResponse { $user = $request->user(); $user->search_design = $request->input('searchDesign') == 'thumbnail'; @@ -84,7 +115,7 @@ class ProfileController extends Controller /** * Update user tag blacklist. */ - public function saveBlacklist(Request $request): \Illuminate\Http\RedirectResponse + public function saveBlacklist(Request $request): RedirectResponse { $user = $request->user(); $tags = json_decode($request->input('tags')); @@ -112,19 +143,60 @@ class ProfileController extends Controller */ public function destroy(Request $request): \Illuminate\Http\RedirectResponse { - $request->validateWithBag('userDeletion', [ - 'password' => ['required', 'current_password'], - ]); - $user = $request->user(); + // Verify password if user has password + if (!is_null($user->password)) { + $request->validateWithBag('userDeletion', [ + 'password' => ['required', 'current_password'], + ]); + } + + // Update comments to deleted user + DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]); + + // Delete Profile Picture + if ($user->avatar) { + Storage::disk('public')->delete($user->avatar); + } + Auth::logout(); $user->delete(); $request->session()->invalidate(); + $request->session()->regenerateToken(); + cache()->flush(); + return Redirect::to('/'); } + + /** + * Store custom user avatar. + */ + protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void + { + // Create Folder for Image Upload + if (! Storage::disk('public')->exists("/images/avatars")) { + Storage::disk('public')->makeDirectory("/images/avatars"); + } + + // Delete old avatar if it exists + if ($user->avatar) { + Storage::disk('public')->delete($user->avatar); + } + + $filename = "images/avatars/{$user->id}.webp"; + + $image = Image::read($file->getRealPath()) + ->cover(128, 128) + ->toWebp(quality: 85); + + Storage::disk('public')->put($filename, $image); + + $user->avatar = $filename; + } + } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php deleted file mode 100644 index 1836c3e..0000000 --- a/app/Http/Controllers/UserController.php +++ /dev/null @@ -1,58 +0,0 @@ -select('id', 'username', 'global_name', 'avatar', 'created_at', 'is_patreon') - ->firstOrFail(); - - return view('user.index', [ - 'user' => $user, - ]); - } - - /** - * Delete User. - */ - public function delete(Request $request): \Illuminate\Http\RedirectResponse - { - $user = User::where('id', $request->user()->id)->firstOrFail(); - - // Delete Playlist - $playlists = Playlist::where('user_id', $user->id)->get(); - foreach($playlists as $playlist) { - PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete(); - $playlist->forceDelete(); - } - - // Update comments to deleted user - DB::table('comments')->where('commenter_id', '=', $user->id)->update(['commenter_id' => 1]); - - $user->forceDelete(); - - Auth::guard('web')->logout(); - - $request->session()->invalidate(); - - $request->session()->regenerateToken(); - - cache()->flush(); - - return redirect('/'); - } -} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 7a19bc0..2574642 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -22,7 +22,7 @@ class LoginRequest extends FormRequest /** * Get the validation rules that apply to the request. * - * @return array + * @return array|string> */ public function rules(): array { @@ -80,6 +80,6 @@ class LoginRequest extends FormRequest */ public function throttleKey(): string { - return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); } } diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php index 327ce6f..d3cdaa8 100644 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -11,13 +11,26 @@ class ProfileUpdateRequest extends FormRequest /** * Get the validation rules that apply to the request. * - * @return array + * @return array|string> */ public function rules(): array { return [ - 'name' => ['string', 'max:255'], - 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + 'name' => ['required', 'string', 'max:255'], + 'image' => [ + 'nullable', + 'image', + 'mimes:jpg,png,jpeg,webp,gif', + 'max:8192' + ], + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore($this->user()->id), + ], ]; } } diff --git a/app/Livewire/AdminCommentSearch.php b/app/Livewire/AdminCommentSearch.php index 0fe21fe..aa49241 100644 --- a/app/Livewire/AdminCommentSearch.php +++ b/app/Livewire/AdminCommentSearch.php @@ -28,9 +28,9 @@ class AdminCommentSearch extends Component { $comments = DB::table('comments') ->join('users', 'comments.commenter_id', '=', 'users.id') - ->select('comments.*', 'users.username') + ->select('comments.*', 'users.name') ->when($this->search !== '', fn ($query) => $query->where('comment', 'LIKE', "%$this->search%")) - ->when($this->userSearch !== '', fn ($query) => $query->where('username', 'LIKE', "%$this->userSearch%")) + ->when($this->userSearch !== '', fn ($query) => $query->where('name', 'LIKE', "%$this->userSearch%")) ->paginate(12); return view('livewire.admin-comment-search', [ diff --git a/app/Livewire/AdminUserSearch.php b/app/Livewire/AdminUserSearch.php index 8680b44..b4b15c1 100644 --- a/app/Livewire/AdminUserSearch.php +++ b/app/Livewire/AdminUserSearch.php @@ -43,10 +43,7 @@ class AdminUserSearch extends Component $users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000)) ->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1)) ->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1)) - ->when($this->search !== '', fn ($query) => $query->where(function($query) { - $query->where('username', 'like', '%'.$this->search.'%') - ->orWhere('global_name', 'like', '%'.$this->search.'%'); - })) + ->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%')) ->paginate(20); return view('livewire.admin-user-search', [ diff --git a/app/Livewire/DownloadsFree.php b/app/Livewire/DownloadsFree.php index 92cd648..a27a385 100644 --- a/app/Livewire/DownloadsFree.php +++ b/app/Livewire/DownloadsFree.php @@ -10,7 +10,6 @@ use Livewire\Component; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cache; class DownloadsFree extends Component { @@ -51,7 +50,7 @@ class DownloadsFree extends Component // Check timestamp if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) { // Already expired - $alreadyDownloaded->forceDelete(); + $alreadyDownloaded->delete(); return; } diff --git a/app/Models/User.php b/app/Models/User.php index 77d4f84..e2c7754 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,20 +2,20 @@ namespace App\Models; +//use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Storage; -use Jakyeru\Larascord\Traits\InteractsWithDiscord; use Laravelista\Comments\Commenter; -use Laravel\Sanctum\HasApiTokens; use Illuminate\Support\Facades\DB; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable, InteractsWithDiscord, Commenter; + use HasFactory, Notifiable, Commenter; /** * The attributes that are mass assignable. @@ -23,22 +23,14 @@ class User extends Authenticatable * @var string[] */ protected $fillable = [ - 'id', - 'username', - 'global_name', - 'discriminator', + 'name', 'email', - 'avatar', - 'verified', - 'banner', - 'banner_color', - 'accent_color', + 'password', 'locale', - 'mfa_enabled', - 'premium_type', - 'public_flags', - 'roles', 'is_banned', + // Discord + 'discord_id', + 'discord_avatar', ]; /** @@ -47,6 +39,7 @@ class User extends Authenticatable * @var array */ protected $hidden = [ + 'password', 'remember_token', ]; @@ -56,24 +49,21 @@ class User extends Authenticatable * @var array */ protected $casts = [ - 'id' => 'integer', - 'username' => 'string', - 'global_name' => 'string', - 'discriminator' => 'string', + // Laravel defaults + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + // Other + 'name' => 'string', 'email' => 'string', - 'avatar' => 'string', - 'verified' => 'boolean', - 'banner' => 'string', - 'banner_color' => 'string', - 'accent_color' => 'string', 'locale' => 'string', - 'mfa_enabled' => 'boolean', - 'premium_type' => 'integer', - 'public_flags' => 'integer', 'roles' => 'json', 'tag_blacklist' => 'array', + // Discord + 'discord_id' => 'integer', + 'discord_avatar' => 'string', ]; + /** * Has Many Playlists. */ @@ -105,4 +95,22 @@ class User extends Authenticatable { return DB::table('comments')->where('commenter_id', $this->id)->count(); } + + /** + * Returns the user avatar image url. + */ + public function getAvatar(): string + { + if ($this->discord_id && $this->discord_avatar && !$this->avatar) + { + return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}"; + } + + if ($this->avatar) + { + return Storage::url($this->avatar); + } + + return asset('images/default-avatar.webp'); + } } diff --git a/app/Override/Comments/CommentService.php b/app/Override/Comments/CommentService.php index e4406b0..b72f260 100644 --- a/app/Override/Comments/CommentService.php +++ b/app/Override/Comments/CommentService.php @@ -91,7 +91,7 @@ class CommentService if (Config::get('comments.soft_deletes') == true) { $comment->delete(); } else { - $comment->forceDelete(); + $comment->delete(); } } @@ -122,12 +122,11 @@ class CommentService $url = '/hentai/' . $episode->slug . '#comment-' . $reply->id; $user = Auth::user(); - $username = $user->global_name ?? $user->username; $parentCommentUser = User::where('id', $comment->commenter_id)->firstOrFail(); $parentCommentUser->notify( new CommentNotification( - "{$username} replied to your comment.", + "{$user->name} replied to your comment.", Str::limit($reply->comment, 50), $url ) diff --git a/app/Override/Discord/DiscordController.php b/app/Override/Discord/DiscordController.php deleted file mode 100644 index a946413..0000000 --- a/app/Override/Discord/DiscordController.php +++ /dev/null @@ -1,155 +0,0 @@ -throwError('missing_guilds_scope'); - } - } - - // Getting the accessToken from the Discord API. - try { - $accessToken = (new DiscordService())->getAccessTokenFromCode($request->get('code')); - } catch (\Exception $e) { - return $this->throwError('invalid_code', $e); - } - - // Get the user from the Discord API. - try { - $user = (new DiscordService())->getCurrentUser($accessToken); - $user->setAccessToken($accessToken); - } catch (\Exception $e) { - return $this->throwError('authorization_failed', $e); - } - - // Making sure the user has an email if the email scope is set. - if (in_array('email', explode('&', config('larascord.scopes')))) { - if (empty($user->email)) { - return $this->throwError('missing_email'); - } - } - - if (auth()->check()) { - // Making sure the current logged-in user's ID is matching the ID retrieved from the Discord API. - if (auth()->id() !== (int)$user->id) { - auth()->logout(); - return $this->throwError('invalid_user'); - } - - // Confirming the session in case the user was redirected from the password.confirm middleware. - $request->session()->put('auth.password_confirmed_at', time()); - } - - // Trying to create or update the user in the database. - // Initiating a database transaction in case something goes wrong. - DB::beginTransaction(); - try { - $user = (new DiscordService())->createOrUpdateUser($user); - $user->accessToken()->updateOrCreate([], $accessToken->toArray()); - } catch (\Exception $e) { - DB::rollBack(); - return $this->throwError('database_error', $e); - } - - // Verifying if the user is soft-deleted. - if (Schema::hasColumn('users', 'deleted_at')) { - if ($user->trashed()) { - DB::rollBack(); - return $this->throwError('user_deleted'); - } - } - - // Patreon check - try { - if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) { - DB::rollBack(); - return $this->throwError('missing_guilds_members_read_scope'); - } - $guildMember = (new DiscordService())->getGuildMember($accessToken, config('discord.guild_id')); - $patreonroles = config('discord.patreon_roles'); - $user->is_patreon = false; - if ((new DiscordService())->hasRoleInGuild($guildMember, $patreonroles)) { - $user->is_patreon = true; - } - $user->save(); - } catch (\Exception $e) { - // Clearly not a patreon - $user->is_patreon = false; - $user->save(); - } - - // Committing the database transaction. - DB::commit(); - - // Authenticating the user if the user is not logged in. - if (!auth()->check()) { - auth()->login($user, config('larascord.remember_me', false)); - } - - // Redirecting the user to the intended page or to the home page. - return redirect()->intended(RouteServiceProvider::HOME); - } - - /** - * Handles the throwing of an error. - */ - private function throwError(string $message, \Exception $exception = NULL): RedirectResponse | JsonResponse - { - if (app()->hasDebugModeEnabled()) { - return response()->json([ - 'larascord_message' => config('larascord.error_messages.' . $message), - 'message' => $exception?->getMessage(), - 'code' => $exception?->getCode() - ]); - } else { - if (config('larascord.error_messages.' . $message . '.redirect')) { - Alert::error('Error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.')); - return redirect(config('larascord.error_messages.' . $message . '.redirect'))->with('error', config('larascord.error_messages.' . $message . '.message', 'An error occurred while trying to log you in.')); - } else { - return redirect('/')->with('error', config('larascord.error_messages.' . $message, 'An error occurred while trying to log you in.')); - } - } - } - - /** - * Handles the deletion of the user. - */ - public function destroy(): RedirectResponse | JsonResponse - { - // Revoking the OAuth2 access token. - try { - (new DiscordService())->revokeAccessToken(auth()->user()->accessToken()->first()->refresh_token); - } catch (\Exception $e) { - return $this->throwError('revoke_token_failed', $e); - } - - // Deleting the user from the database. - auth()->user()->delete(); - - // Showing the success message. - if (config('larascord.success_messages.user_deleted.redirect')) { - return redirect(config('larascord.success_messages.user_deleted.redirect'))->with('success', config('larascord.success_messages.user_deleted.message', 'Your account has been deleted.')); - } else { - return redirect('/')->with('success', config('larascord.success_messages.user_deleted', 'Your account has been deleted.')); - } - } -} diff --git a/app/Override/Discord/Services/DiscordService.php b/app/Override/Discord/Services/DiscordService.php deleted file mode 100644 index 599d969..0000000 --- a/app/Override/Discord/Services/DiscordService.php +++ /dev/null @@ -1,273 +0,0 @@ - NULL, - "client_secret" => NULL, - "grant_type" => "authorization_code", - "code" => NULL, - "redirect_uri" => NULL, - "scope" => null - ]; - - /** - * UserService constructor. - */ - public function __construct() - { - $this->tokenData['client_id'] = config('larascord.client_id'); - $this->tokenData['client_secret'] = config('larascord.client_secret'); - $this->tokenData['grant_type'] = config('larascord.grant_type'); - $this->tokenData['redirect_uri'] = config('larascord.redirect_uri'); - $this->tokenData['scope'] = config('larascord.scopes'); - } - - /** - * Handles the Discord OAuth2 callback and returns the access token. - * - * @throws RequestException - */ - public function getAccessTokenFromCode(string $code): AccessToken - { - $this->tokenData['code'] = $code; - - $response = Http::asForm()->post($this->tokenURL, $this->tokenData); - - $response->throw(); - - return new AccessToken(json_decode($response->body())); - } - - /** - * Get access token from refresh token. - * - * @throws RequestException - */ - public function refreshAccessToken(string $refreshToken): AccessToken - { - $response = Http::asForm()->post($this->tokenURL, [ - 'client_id' => config('larascord.client_id'), - 'client_secret' => config('larascord.client_secret'), - 'grant_type' => 'refresh_token', - 'refresh_token' => $refreshToken, - ]); - - $response->throw(); - - return new AccessToken(json_decode($response->body())); - } - - /** - * Authenticates the user with the access token and returns the user data. - * - * @throws RequestException - */ - public function getCurrentUser(AccessToken $accessToken): \Jakyeru\Larascord\Types\User - { - $response = Http::withToken($accessToken->access_token)->get($this->baseApi . '/users/@me'); - - $response->throw(); - - return new \Jakyeru\Larascord\Types\User(json_decode($response->body())); - } - - /** - * Get the user's guilds. - * - * @throws RequestException - * @throws Exception - */ - public function getCurrentUserGuilds(AccessToken $accessToken, bool $withCounts = false): array - { - if (!$accessToken->hasScope('guilds')) throw new Exception(config('larascord.error_messages.missing_guilds_scope.message')); - - $endpoint = '/users/@me/guilds'; - - if ($withCounts) { - $endpoint .= '?with_counts=true'; - } - - $response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . $endpoint); - - $response->throw(); - - return array_map(function ($guild) { - return new \Jakyeru\Larascord\Types\Guild($guild); - }, json_decode($response->body())); - } - - /** - * Get the Guild Member object for a user. - * - * @throws RequestException - * @throws Exception - */ - public function getGuildMember(AccessToken $accessToken, string $guildId): GuildMember - { - if (!$accessToken->hasScopes(['guilds', 'guilds.members.read'])) throw new Exception(config('larascord.error_messages.missing_guilds_members_read_scope.message')); - - $response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/guilds/' . $guildId . '/member'); - - $response->throw(); - - return new GuildMember(json_decode($response->body())); - } - - /** - * Get the User's connections. - * - * @throws RequestException - * @throws Exception - */ - public function getCurrentUserConnections(AccessToken $accessToken): array - { - if (!$accessToken->hasScope('connections')) throw new Exception('The "connections" scope is required.'); - - $response = Http::withToken($accessToken->access_token, $accessToken->token_type)->get($this->baseApi . '/users/@me/connections'); - - $response->throw(); - - return array_map(function ($connection) { - return new \Jakyeru\Larascord\Types\Connection($connection); - }, json_decode($response->body())); - } - - /** - * Join a guild. - * - * @throws RequestException - * @throws Exception - */ - public function joinGuild(AccessToken $accessToken, User $user, string $guildId, array $options = []): GuildMember - { - if (!config('larascord.access_token')) throw new Exception(config('larascord.error_messages.missing_access_token.message')); - if (!$accessToken->hasScope('guilds.join')) throw new Exception('The "guilds" and "guilds.join" scopes are required.'); - - $response = Http::withToken(config('larascord.access_token'), 'Bot')->put($this->baseApi . '/guilds/' . $guildId . '/members/' . $user->id, array_merge([ - 'access_token' => $accessToken->access_token, - ], $options)); - - $response->throw(); - - if ($response->status() === 204) return throw new Exception('User is already in the guild.'); - - return new GuildMember(json_decode($response->body())); - } - - /** - * Create or update a user in the database. - * - * @throws Exception - */ - public function createOrUpdateUser(\Jakyeru\Larascord\Types\User $user): User - { - if (!$user->getAccessToken()) { - throw new Exception('User access token is missing.'); - } - - $forgottenUser = User::where('email', '=', $user->email)->where('id', '!=', $user->id)->first(); - if ($forgottenUser) { - // This case should never happen (TM) - The discord id changed - // The user probably re-created their discord account with the same email - - // Delete Playlist - $playlists = Playlist::where('user_id', $forgottenUser->id)->get(); - foreach($playlists as $playlist) { - PlaylistEpisode::where('playlist_id', $playlist->id)->forceDelete(); - $playlist->forceDelete(); - } - - // Update comments to deleted user - DB::table('comments')->where('commenter_id', '=', $forgottenUser->id)->update(['commenter_id' => 1]); - - $forgottenUser->forceDelete(); - } - - return User::updateOrCreate( - [ - 'id' => $user->id, - ], - $user->toArray(), - ); - } - - /** - * Verify if the user is in the specified guild(s). - */ - public function isUserInGuilds(array $guilds): bool - { - // Verify if the user is in all the specified guilds if strict mode is enabled. - if (config('larascord.guilds_strict')) { - return empty(array_diff(config('larascord.guilds'), array_column($guilds, 'id'))); - } - - // Verify if the user is in any of the specified guilds if strict mode is disabled. - return !empty(array_intersect(config('larascord.guilds'), array_column($guilds, 'id'))); - } - - /** - * Verify if the user has the specified role(s) in the specified guild. - */ - public function hasRoleInGuild(GuildMember $guildMember, array $roles): bool - { - // Verify if the user has any of the specified roles. - return !empty(array_intersect($roles, $guildMember->roles)); - } - - /** - * Updates the user's roles in the database. - */ - public function updateUserRoles(User $user, GuildMember $guildMember, int $guildId): void - { - // Updating the user's roles in the database. - $updatedRoles = $user->roles; - $updatedRoles[$guildId] = $guildMember->roles; - $user->roles = $updatedRoles; - $user->save(); - } - - /** - * Revoke the user's access token. - * - * @throws RequestException - */ - public function revokeAccessToken(string $accessToken): object - { - $response = Http::asForm()->post($this->tokenURL . '/revoke', [ - 'token' => $accessToken, - 'client_id' => config('larascord.client_id'), - 'client_secret' => config('larascord.client_secret'), - ]); - - $response->throw(); - - return json_decode($response->body()); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..805eeee 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { + $event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class); + }); } } diff --git a/app/Services/GalleryService.php b/app/Services/GalleryService.php index 8e40073..50af35c 100644 --- a/app/Services/GalleryService.php +++ b/app/Services/GalleryService.php @@ -74,7 +74,7 @@ class GalleryService foreach ($oldGallery as $oldImage) { Storage::disk('public')->delete($oldImage->image_url); Storage::disk('public')->delete($oldImage->thumbnail_url); - $oldImage->forceDelete(); + $oldImage->delete(); } } } diff --git a/composer.json b/composer.json index 0bd9806..d0233f2 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,10 @@ "http-interop/http-factory-guzzle": "^1.2", "intervention/image": "^3.9", "intervention/image-laravel": "^1.3", - "jakyeru/larascord": "^6.0", "laravel/framework": "^11.0", "laravel/sanctum": "^4.0", "laravel/scout": "^10.20", + "laravel/socialite": "^5.24", "laravel/tinker": "^2.10", "laravelista/comments": "dev-l11-compatibility", "livewire/livewire": "^3.6.4", @@ -27,6 +27,7 @@ "predis/predis": "^2.2", "realrashid/sweet-alert": "^7.2", "rtconner/laravel-tagging": "^4.1", + "socialiteproviders/discord": "^4.2", "spatie/laravel-discord-alerts": "^1.5", "spatie/laravel-sitemap": "^7.3", "vluzrmos/language-detector": "^2.3" @@ -34,6 +35,7 @@ "require-dev": { "barryvdh/laravel-debugbar": "^3.14.7", "fakerphp/faker": "^1.24.0", + "laravel/breeze": "^2.3", "laravel/pint": "^1.18", "laravel/sail": "^1.38", "mockery/mockery": "^1.4.4", @@ -49,8 +51,6 @@ ], "autoload": { "exclude-from-classmap": [ - "vendor/jakyeru/larascord/src/Http/Services/DiscordService.php", - "vendor/jakyeru/larascord/src/Http/Controllers/DiscordController.php", "vendor/laravelista/comments/src/CommentPolicy.php", "vendor/laravelista/comments/src/CommentService.php" ], @@ -60,8 +60,6 @@ "Database\\Seeders\\": "database/seeders/" }, "files": [ - "app/Override/Discord/Services/DiscordService.php", - "app/Override/Discord/DiscordController.php", "app/Override/Comments/CommentPolicy.php", "app/Override/Comments/CommentService.php" ] diff --git a/composer.lock b/composer.lock index dc35e8f..cedb267 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "484d21a7c10b1609a22d642e71a71cc3", + "content-hash": "749225dc4ea2aca06f1639bef889cc59", "packages": [ { "name": "brick/math", @@ -631,6 +631,69 @@ }, "time": "2019-12-30T22:54:17+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v7.0.2" + }, + "time": "2025-12-16T22:17:28+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -1533,59 +1596,6 @@ ], "time": "2025-04-04T15:09:55+00:00" }, - { - "name": "jakyeru/larascord", - "version": "v6.0.3", - "source": { - "type": "git", - "url": "https://github.com/JakyeRU/Larascord.git", - "reference": "d1099d1418022eda970fec4f13634ee5fbee93b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/JakyeRU/Larascord/zipball/d1099d1418022eda970fec4f13634ee5fbee93b3", - "reference": "d1099d1418022eda970fec4f13634ee5fbee93b3", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^7.5", - "laravel/breeze": "^v2.0", - "laravel/framework": "^11", - "php": "^8.2|^8.3" - }, - "require-dev": { - "orchestra/testbench": "^9" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Jakyeru\\Larascord\\LarascordServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Jakyeru\\Larascord\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jakye", - "email": "jakyeru@gmail.com" - } - ], - "description": "Larascord is a package that allows you to authenticate users in your Laravel application using Discord.", - "support": { - "issues": "https://github.com/JakyeRU/Larascord/issues", - "source": "https://github.com/JakyeRU/Larascord/tree/v6.0.3" - }, - "time": "2025-06-18T18:41:42+00:00" - }, { "name": "jaybizzle/crawler-detect", "version": "v1.3.6", @@ -1638,67 +1648,6 @@ }, "time": "2025-09-30T16:22:43+00:00" }, - { - "name": "laravel/breeze", - "version": "v2.3.8", - "source": { - "type": "git", - "url": "https://github.com/laravel/breeze.git", - "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/breeze/zipball/1a29c5792818bd4cddf70b5f743a227e02fbcfcd", - "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd", - "shasum": "" - }, - "require": { - "illuminate/console": "^11.0|^12.0", - "illuminate/filesystem": "^11.0|^12.0", - "illuminate/support": "^11.0|^12.0", - "illuminate/validation": "^11.0|^12.0", - "php": "^8.2.0", - "symfony/console": "^7.0" - }, - "require-dev": { - "laravel/framework": "^11.0|^12.0", - "orchestra/testbench-core": "^9.0|^10.0", - "phpstan/phpstan": "^2.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Breeze\\BreezeServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Breeze\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", - "keywords": [ - "auth", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel/breeze/issues", - "source": "https://github.com/laravel/breeze" - }, - "time": "2025-07-18T18:49:59+00:00" - }, { "name": "laravel/framework", "version": "v11.46.1", @@ -2179,6 +2128,78 @@ }, "time": "2025-09-22T17:29:40+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.24.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "25e28c14d55404886777af1d77cf030e0f633142" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142", + "reference": "25e28c14d55404886777af1d77cf030e0f633142", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4|^7.0", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "league/oauth1-client": "^1.11", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.18|^5.20|^6.47|^7.55|^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5|^12.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2026-01-01T02:57:21+00:00" + }, { "name": "laravel/tinker", "version": "v2.10.1", @@ -2686,6 +2707,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, { "name": "league/pipeline", "version": "1.1.0", @@ -4038,6 +4135,125 @@ ], "time": "2025-05-08T08:14:37+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "php-http/discovery", "version": "1.20.0", @@ -4192,6 +4408,116 @@ ], "time": "2025-08-21T11:53:16+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.48", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2025-12-15T11:51:42+00:00" + }, { "name": "predis/predis", "version": "v2.4.0", @@ -5147,6 +5473,130 @@ }, "time": "2022-04-25T22:18:50+00:00" }, + { + "name": "socialiteproviders/discord", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Discord.git", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Discord\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Discord OAuth2 Provider for Laravel Socialite", + "keywords": [ + "discord", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/discord", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-07-24T23:28:47+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v4.8.1", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/8180ec14bef230ec2351cff993d5d2d7ca470ef4", + "reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "laravel/socialite": "^5.5", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "SocialiteProviders\\Manager\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SocialiteProviders\\Manager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Wendt", + "email": "andy@awendt.com" + }, + { + "name": "Anton Komarev", + "email": "a.komarev@cybercog.su" + }, + { + "name": "Miguel Piedrafita", + "email": "soy@miguelpiedrafita.com" + }, + { + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" + } + ], + "description": "Easily add new or override built-in providers in Laravel Socialite.", + "homepage": "https://socialiteproviders.com", + "keywords": [ + "laravel", + "manager", + "oauth", + "providers", + "socialite" + ], + "support": { + "issues": "https://github.com/socialiteproviders/manager/issues", + "source": "https://github.com/socialiteproviders/manager" + }, + "time": "2025-02-24T19:33:30+00:00" + }, { "name": "spatie/browsershot", "version": "5.0.11", @@ -8835,6 +9285,67 @@ }, "time": "2025-04-30T06:54:44+00:00" }, + { + "name": "laravel/breeze", + "version": "v2.3.8", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/1a29c5792818bd4cddf70b5f743a227e02fbcfcd", + "reference": "1a29c5792818bd4cddf70b5f743a227e02fbcfcd", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0", + "illuminate/filesystem": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "illuminate/validation": "^11.0|^12.0", + "php": "^8.2.0", + "symfony/console": "^7.0" + }, + "require-dev": { + "laravel/framework": "^11.0|^12.0", + "orchestra/testbench-core": "^9.0|^10.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2025-07-18T18:49:59+00:00" + }, { "name": "laravel/pint", "version": "v1.25.1", diff --git a/config/discord.php b/config/discord.php index 8d32b62..bac3bad 100644 --- a/config/discord.php +++ b/config/discord.php @@ -1,10 +1,19 @@ 'https://discord.gg/yAqgVKNgG5', 'guild_id' => 802233383710228550, - 'patreon_roles' => [841798154999169054, 803329707650187364, 803327903659196416, 803325441942356059, 803322725576736858, 802270568912519198, 802234830384267315], + 'patreon_roles' => [ + 841798154999169054, // ???? + 803329707650187364, // Tier-5 + 803327903659196416, // ???? + 803325441942356059, // Tier-3 + 803322725576736858, // Tier-2 + 802270568912519198, // Tier-1 + 802234830384267315 // admin + ], + + 'discord_bot_token' => env('DISCORD_BOT_TOKEN'), ]; diff --git a/config/larascord.php b/config/larascord.php deleted file mode 100644 index dd94be1..0000000 --- a/config/larascord.php +++ /dev/null @@ -1,247 +0,0 @@ - env('LARASCORD_CLIENT_ID', null), - - /* - |-------------------------------------------------------------------------- - | Application Secret - |-------------------------------------------------------------------------- - | - | This is the secret of your Discord application. - | - */ - - 'client_secret' => env('LARASCORD_CLIENT_SECRET', null), - - /* - |-------------------------------------------------------------------------- - | Application Access Token - |-------------------------------------------------------------------------- - | - | This is the access token of your Discord application. - | - */ - - 'access_token' => env('LARASCORD_ACCESS_TOKEN', null), - - /* - |-------------------------------------------------------------------------- - | Grant Type - |-------------------------------------------------------------------------- - | - | This is the grant type of your Discord application. It must be set to - | "authorization_code". - | - */ - - 'grant_type' => env('LARASCORD_GRANT_TYPE', 'authorization_code'), - - /* - |-------------------------------------------------------------------------- - | Redirect URI - |-------------------------------------------------------------------------- - | - | This is the URI that Discord will redirect to after the user authorizes - | your application. - | - */ - - 'redirect_uri' => env('APP_URL', 'http://localhost:8000') . '/' . env('LARASCORD_PREFIX', 'larascord') . '/callback', - - /* - |-------------------------------------------------------------------------- - | Scopes - |-------------------------------------------------------------------------- - | - | These are the OAuth2 scopes of your Discord application. - | - */ - - 'scopes' => env('LARASCORD_SCOPE', 'identify&email&guilds&guilds.members.read'), - - /* - |-------------------------------------------------------------------------- - | Route Prefix - |-------------------------------------------------------------------------- - | - | This is the prefix that Larascord will use for its routes. For example, - | the prefix "larascord" will result in the route - | "https://domain.com/larascord/login". - | - */ - - 'route_prefix' => env('LARASCORD_PREFIX', 'larascord'), - - /* - |-------------------------------------------------------------------------- - | OAuth2 Prompt - "none" or "consent" - |-------------------------------------------------------------------------- - | - | The prompt controls how the authorization flow handles existing authorizations. - | If a user has previously authorized your application with the requested scopes - | and prompt is set to consent,it will request them to re-approve their - | authorization. If set to none, it will skip the authorization screen - | and redirect them back to your redirect URI without requesting - | their authorization. - | - */ - - 'prompt' => 'none', - - /* - |-------------------------------------------------------------------------- - | Restrict Access to Specific Guilds - |-------------------------------------------------------------------------- - | - | This option restricts access to the application to users who are members - | of specific Discord guilds. Users who are not members of the specified - | guilds will not be able to use the application. - | - */ - - 'guilds' => [], - - /* - |-------------------------------------------------------------------------- - | Restrict Access to Specific Guilds - Strict Mode - |-------------------------------------------------------------------------- - | - | Enabling this option will require the user to be a member of ALL the - | aforementioned guilds. If this option is disabled, the user will - | only need to be a member of at least ONE of the guilds. - | - */ - - 'guilds_strict' => false, - - /* - |-------------------------------------------------------------------------- - | Restrict Access to Specific Roles - |-------------------------------------------------------------------------- - | - | When this option is enabled, the user will only be able to use the - | application if they have at least one of the specified roles. - | - */ - - // WARNING: This feature makes one request to the Discord API for each guild you specify. (Because you need to fetch the roles for each guild) - // At the moment the database is not checked for roles when the user logs in. It will always fetch the roles from the Discord API. - // Currently, the roles are only updated in the database when the user logs in. The roles from the database can be used in a middleware. - // I'm working on a better way to do this, but for now, this will work. - - 'guild_roles' => [ - // 'guild_id' => [ - // 'role_id', - // 'role_id', - // ], - ], - - /* - |-------------------------------------------------------------------------- - | Remember Me - |-------------------------------------------------------------------------- - | - | Whether or not to remember the user after they log in. - | - */ - - 'remember_me' => true, - - /* - |-------------------------------------------------------------------------- - | Error Messages - |-------------------------------------------------------------------------- - | - | These are the error messages that will be displayed to the user if there - | is an error. - | - */ - - 'error_messages' => [ - 'missing_code' => [ - 'message' => 'The authorization code is missing.', - 'redirect' => '/' - ], - 'invalid_code' => [ - 'message' => 'The authorization code is invalid.', - 'redirect' => '/' - ], - 'authorization_failed' => [ - 'message' => 'The authorization failed.', - 'redirect' => '/' - ], - 'missing_email' => [ - 'message' => 'Couldn\'t get your e-mail address. Please add an e-mail address to your Discord account!', - 'redirect' => '/' - ], - 'invalid_user' => [ - 'message' => 'The user ID doesn\'t match the logged-in user.', - 'redirect' => '/' - ], - 'database_error' => [ - 'message' => 'There was an error with the database. Please try again later.', - 'redirect' => '/' - ], - 'missing_guilds_scope' => [ - 'message' => 'The "guilds" scope is required.', - 'redirect' => '/' - ], - 'missing_guilds_members_read_scope' => [ - 'message' => 'The "guilds" and "guilds.members.read" scopes are required.', - 'redirect' => '/' - ], - 'authorization_failed_guilds' => [ - 'message' => 'Couldn\'t get the servers you\'re in.', - 'redirect' => '/' - ], - 'not_member_guild_only' => [ - 'message' => 'You are not a member of the required guilds.', - 'redirect' => '/' - ], - 'missing_access_token' => [ - 'message' => 'The access token is missing.', - 'redirect' => '/' - ], - 'authorization_failed_roles' => [ - 'message' => 'Couldn\'t get the roles you have.', - 'redirect' => '/' - ], - 'missing_role' => [ - 'message' => 'You don\'t have the required roles.', - 'redirect' => '/' - ], - 'revoke_token_failed' => [ - 'message' => 'An error occurred while trying to revoke your access token.', - 'redirect' => '/' - ], - ], - - /* - |-------------------------------------------------------------------------- - | Success Messages - |-------------------------------------------------------------------------- - | - | These are the success messages that will be displayed to the user if there - | is no error. - | - */ - - 'success_messages' => [ - 'user_deleted' => [ - 'message' => 'Your account has been deleted.', - 'redirect' => '/' - ], - ], - -]; diff --git a/config/services.php b/config/services.php index 0ace530..b02de0e 100644 --- a/config/services.php +++ b/config/services.php @@ -31,4 +31,21 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + /* + |-------------------------------------------------------------------------- + | Socialite Providers + |-------------------------------------------------------------------------- + */ + + 'discord' => [ + 'client_id' => env('DISCORD_CLIENT_ID'), + 'client_secret' => env('DISCORD_CLIENT_SECRET'), + 'redirect' => '/auth/discord/callback', + + // optional + 'allow_gif_avatars' => (bool) env('DISCORD_AVATAR_GIF', true), + 'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'webp'), // only pick from jpg, png, webp + ], + + ]; diff --git a/database/migrations/2025_09_21_215923_add_unique_constraint_to_downloads_table.php b/database/migrations/2025_09_21_215923_add_unique_constraint_to_downloads_table.php index 2dab96b..f62f990 100644 --- a/database/migrations/2025_09_21_215923_add_unique_constraint_to_downloads_table.php +++ b/database/migrations/2025_09_21_215923_add_unique_constraint_to_downloads_table.php @@ -5,6 +5,7 @@ use App\Models\Downloads; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Facades\DB; return new class extends Migration { @@ -14,7 +15,7 @@ return new class extends Migration public function up(): void { # Delete entries with "#" as URL - Downloads::where('url', '#')->forceDelete(); + Downloads::where('url', '#')->delete(); # Remove duplicate entries $duplicates = DB::table('downloads') diff --git a/database/migrations/2026_01_06_161620_fix_discord_oauth_system.php b/database/migrations/2026_01_06_161620_fix_discord_oauth_system.php new file mode 100644 index 0000000..8fd33ab --- /dev/null +++ b/database/migrations/2026_01_06_161620_fix_discord_oauth_system.php @@ -0,0 +1,65 @@ +dropColumn('discriminator'); + $table->dropColumn('remember_token'); + $table->dropColumn('banner'); + $table->dropColumn('banner_color'); + $table->dropColumn('accent_color'); + $table->dropColumn('premium_type'); + $table->dropColumn('public_flags'); + $table->dropColumn('verified'); + $table->dropColumn('mfa_enabled'); + $table->dropColumn('global_name'); + $table->dropColumn('locale'); + }); + + // Change & Add Columns + Schema::table('users', function (Blueprint $table) { + // Rename + $table->renameColumn('username', 'name'); + $table->renameColumn('avatar', 'discord_avatar'); + $table->string('avatar')->nullable()->after('email'); + + // Re-Add Email verification + $table->timestamp('email_verified_at')->nullable()->after('email'); + + // Re-Add Password Auth + $table->string('password')->nullable()->after('email_verified_at'); + $table->rememberToken()->after('password'); + }); + + + /** + * -------------------------------------------------------------------- + * Fix Discord Profile Pictures + * -------------------------------------------------------------------- + * The oauth package by socialite now returns a full url of the avatar. + * Meaning all the old entries have to be fixed. + */ + foreach (User::whereNotNull('discord_avatar')->get() as $user) + { + $isGif = preg_match('/a_.+/m', $user->discord_avatar) === 1; + $extension = $isGif ? 'gif' : 'webp'; + $user->discord_avatar = sprintf('https://cdn.discordapp.com/avatars/%s/%s.%s', $user->id, $user->discord_avatar, $extension); + $user->save(); + } + } +}; diff --git a/database/migrations/2026_01_08_213625_fix_database_structure.php b/database/migrations/2026_01_08_213625_fix_database_structure.php new file mode 100644 index 0000000..304c3c7 --- /dev/null +++ b/database/migrations/2026_01_08_213625_fix_database_structure.php @@ -0,0 +1,207 @@ +unsignedBigInteger('discord_id')->nullable()->after('id'); + }); + + // 2. Migrate Discord Users IDs + DB::table('users') + ->where('id', '>', 10000) + ->update(['discord_id' => DB::raw('id')]); + + // 3. Temporary new auto increment column + Schema::table('users', function (Blueprint $table) { + $table->unsignedBigInteger('new_id')->first(); + }); + + // 3.5 Count + DB::statement(' + UPDATE users u + JOIN ( + SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn + FROM users + ) t ON u.id = t.id + SET u.new_id = t.rn + '); + + // 4. Drop foreign keys + $this->dropForeignKeys(); + + // 5. Fix ID's in other tables + $this->updateUserIDsInOtherTables(); + + // 6. Remove old ID + Schema::table('users', function (Blueprint $table) { + $table->bigInteger('id')->unsigned()->change(); + $table->dropPrimary('id'); + $table->dropColumn('id'); + }); + + // 7. Rename new_id to id + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('new_id', 'id'); + }); + + // 8. Change new ID to auto increment and set as primary key + Schema::table('users', function (Blueprint $table) { + $table->unsignedBigInteger('id')->autoIncrement()->primary()->change(); + }); + + // 9. Remove data that would conflict with constraints + $this->deleteUnreferencedData(); + + // 9. Recreate foreign key constraints + $this->addForeignKeys(); + } + + /** + * Drop Foreign Keys referencing the user id + */ + private function dropForeignKeys(): void + { + Schema::table('markable_likes', function (Blueprint $table) { + $table->dropForeign(['user_id']); + }); + + Schema::table('watched', function (Blueprint $table) { + $table->dropForeign(['user_id']); + }); + + // Our Schema does include a foreign key, for whatever reason it doesn't exist in the first palce + // Schema::table('user_downloads', function (Blueprint $table) { + // $table->dropForeign(['user_id']); + // }); + } + + /** + * Tables to fix the IDs: + * - comments ['commenter_id'] + * - markable_likes ['user_id'] + * - notifications ['notifiable_id'] + * - playlists ['user_id'] + * - user_downloads ['user_id'] + * - watched ['user_id'] + */ + private function updateUserIDsInOtherTables(): void + { + DB::statement(' + UPDATE comments c + JOIN users u ON c.commenter_id = u.id + SET c.commenter_id = u.new_id + '); + + DB::statement(' + UPDATE watched w + JOIN users u ON w.user_id = u.id + SET w.user_id = u.new_id + '); + + DB::statement(' + UPDATE markable_likes ml + JOIN users u ON ml.user_id = u.id + SET ml.user_id = u.new_id + '); + + DB::statement(' + UPDATE notifications n + JOIN users u ON n.notifiable_id = u.id + SET n.notifiable_id = u.new_id + '); + + DB::statement(' + UPDATE playlists p + JOIN users u ON p.user_id = u.id + SET p.user_id = u.new_id + '); + + DB::statement(' + UPDATE user_downloads ud + JOIN users u ON ud.user_id = u.id + SET ud.user_id = u.new_id + '); + } + + /** + * Due to incorrect handling of user deletes, + * we have unreferenced data + */ + private function deleteUnreferencedData(): void + { + // User Downloads Table + DB::table('user_downloads') + ->where('user_id', '>', 1_000_000) + ->delete(); + + // User Playlists Table + $playlists = Playlist::where('user_id', '>', 1_000_000) + ->get(); + + foreach($playlists as $playlist) { + DB::table('playlist_episodes') + ->where('playlist_id', '=', $playlist->id) + ->delete(); + + $playlist->delete(); + } + } + + /** + * Re-Add Foreign Keys to tables which we dropped previously + */ + private function addForeignKeys(): void + { + Schema::table('markable_likes', function (Blueprint $table) { + // Ensure the column is unsigned + $table->bigInteger('user_id')->unsigned()->change(); + + // Add the foreign key constraint + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::table('watched', function (Blueprint $table) { + // Ensure the column is unsigned + $table->bigInteger('user_id')->unsigned()->change(); + + // Add the foreign key constraint + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::table('user_downloads', function (Blueprint $table) { + // Ensure the column is unsigned + $table->bigInteger('user_id')->unsigned()->change(); + + // Add the foreign key constraint + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::table('playlist_episodes', function (Blueprint $table) { + // Ensure the column is unsigned + $table->bigInteger('playlist_id')->unsigned()->change(); + + // Add the foreign key constraint + $table->foreign('playlist_id')->references('id')->on('playlists')->onDelete('cascade'); + }); + + Schema::table('playlists', function (Blueprint $table) { + // Ensure the column is unsigned + $table->bigInteger('user_id')->unsigned()->change(); + + // Add the foreign key constraint + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index 246257e..5984385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,13 @@ "vidstack": "^1.12.13" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "autoprefixer": "^10.4.18", + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.4.2", + "autoprefixer": "^10.4.2", "axios": "^1.6.8", "laravel-vite-plugin": "^2.0.0", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", "vite": "^7.1.6", "vite-plugin-static-copy": "^3.0.1" } @@ -974,6 +975,23 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "dev": true, + "license": "MIT" + }, "node_modules/@yaireo/tagify": { "version": "4.35.4", "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.35.4.tgz", @@ -1001,6 +1019,16 @@ "node": ">=0.4.0" } }, + "node_modules/alpinejs": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.3.tgz", + "integrity": "sha512-fSI6F5213FdpMC4IWaup92KhuH3jBX0VVqajRJ6cOTCy1cL6888KyXdGO+seAAkn+g6fnrxBqQEx6gRpQ5EZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", diff --git a/package.json b/package.json index da8f3b1..bbb40f4 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,13 @@ "build": "vite build" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.7", - "autoprefixer": "^10.4.18", + "@tailwindcss/forms": "^0.5.2", + "alpinejs": "^3.4.2", + "autoprefixer": "^10.4.2", "axios": "^1.6.8", "laravel-vite-plugin": "^2.0.0", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", + "postcss": "^8.4.31", + "tailwindcss": "^3.1.0", "vite": "^7.1.6", "vite-plugin-static-copy": "^3.0.1" }, diff --git a/resources/js/app.js b/resources/js/app.js index 935b972..01c671a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,8 +1,5 @@ import './bootstrap'; - -// import { Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm'; -// Alpine.start(); - +import 'hammerjs'; import { Collapse, Carousel, @@ -15,6 +12,10 @@ import { initTE, } from "tw-elements"; -initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple }); +import Alpine from 'alpinejs'; -import 'hammerjs'; +window.Alpine = Alpine; + +Alpine.start(); + +initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple }); diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php index 6969370..f462403 100644 --- a/resources/views/auth/confirm-password.blade.php +++ b/resources/views/auth/confirm-password.blade.php @@ -1,20 +1,29 @@ -
- {{ __('This is a secure area of the application. Please confirm your session before continuing.') }} -
- -
- @csrf - -
- - - - - - - {{ __('Confirm') }} - +
+
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
- - \ No newline at end of file + +
+ @csrf + + +
+ + + + + +
+ +
+ + {{ __('Confirm') }} + +
+
+
+ diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..0ece4d3 --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,27 @@ + +
+
+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} +
+ + + + +
+ @csrf + + +
+ + + +
+ +
+ + {{ __('Send Password Reset Link') }} + +
+
+
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..f422cda --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,138 @@ + + + + + + + + + + diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..5ae3b8d --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,41 @@ + +
+
+ @csrf + + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
+
diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..f0b011b --- /dev/null +++ b/resources/views/auth/verify-email.blade.php @@ -0,0 +1,33 @@ + +
+
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ @csrf + + +
+
+
+
diff --git a/resources/views/components/application-logo.blade.php b/resources/views/components/application-logo.blade.php index 03cdb26..bb57f69 100644 --- a/resources/views/components/application-logo.blade.php +++ b/resources/views/components/application-logo.blade.php @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/views/components/dropdown-link.blade.php b/resources/views/components/dropdown-link.blade.php index 083548e..20b8f36 100644 --- a/resources/views/components/dropdown-link.blade.php +++ b/resources/views/components/dropdown-link.blade.php @@ -1 +1 @@ -merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }} +merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-neutral-100 dark:hover:bg-neutral-900 focus:outline-none focus:bg-neutral-100 dark:focus:bg-neutral-800 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/resources/views/components/dropdown.blade.php b/resources/views/components/dropdown.blade.php index 4a68c9e..bbb5d63 100644 --- a/resources/views/components/dropdown.blade.php +++ b/resources/views/components/dropdown.blade.php @@ -1,24 +1,16 @@ @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-neutral-800']) @php -switch ($align) { - case 'left': - $alignmentClasses = 'origin-top-left left-0'; - break; - case 'top': - $alignmentClasses = 'origin-top'; - break; - case 'right': - default: - $alignmentClasses = 'origin-top-right right-0'; - break; -} +$alignmentClasses = match ($align) { + 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', + 'top' => 'origin-top', + default => 'ltr:origin-top-right rtl:origin-top-left end-0', +}; -switch ($width) { - case '48': - $width = 'w-48'; - break; -} +$width = match ($width) { + '48' => 'w-48', + default => $width, +}; @endphp
@@ -28,11 +20,11 @@ switch ($width) { + +
+ {{ __('Save') }} + + @if (session('status') === 'profile-updated') +

{{ __('Saved.') }}

+ @endif +
+ diff --git a/resources/views/profile/settings.blade.php b/resources/views/profile/settings.blade.php index cac2e65..81c4ac5 100644 --- a/resources/views/profile/settings.blade.php +++ b/resources/views/profile/settings.blade.php @@ -8,13 +8,16 @@
@include('user.partials.background') -
+
@include('profile.partials.sidebar') -
+
@include('profile.partials.update-profile-information-form')
+
+ @include('profile.partials.update-password-form') +
@include('profile.partials.update-blacklist-form')
@@ -24,6 +27,7 @@
@include('profile.partials.delete-user-form')
+ @include('profile.partials.delete-user-modal')
@vite(['resources/js/user-blacklist.js'])
diff --git a/resources/views/stream/partials/playlist.blade.php b/resources/views/stream/partials/playlist.blade.php index a60fd58..98f20e6 100644 --- a/resources/views/stream/partials/playlist.blade.php +++ b/resources/views/stream/partials/playlist.blade.php @@ -22,7 +22,7 @@ } @endphp

- {{ $playlist->user->global_name ?? $playlist->user->username }} • {{ $currentIndex + 1 }}/{{ $episodeCount }} Episodes + {{ $playlist->user->name }} • {{ $currentIndex + 1 }}/{{ $episodeCount }} Episodes

diff --git a/resources/views/user/partials/profile.blade.php b/resources/views/user/partials/profile.blade.php index c90ba50..4d1d22e 100644 --- a/resources/views/user/partials/profile.blade.php +++ b/resources/views/user/partials/profile.blade.php @@ -1,14 +1,9 @@
- @if($user->avatar) - - @else - - @endif +
- {{ $user->global_name ?? $user->username }} + {{ $user->name }} @if ($user->is_patreon) diff --git a/resources/views/vendor/comments/_comment.blade.php b/resources/views/vendor/comments/_comment.blade.php index 3ffcbe0..ac77b9f 100644 --- a/resources/views/vendor/comments/_comment.blade.php +++ b/resources/views/vendor/comments/_comment.blade.php @@ -7,22 +7,18 @@
- - @if($comment->commenter->avatar) - {{ $comment->commenter->global_name ?? $comment->commenter->username }} Avatar - @else - {{ $comment->commenter->global_name ?? $comment->commenter->username }} Avatar - @endif - +
+ {{ $comment->commenter->name }} Avatar +
- +
@if($comment->commenter->is_patreon) -
{{ $comment->commenter->global_name ?? $comment->commenter->username }} - {{ $comment->created_at->diffForHumans() }}
+
{{ $comment->commenter->name }} - {{ $comment->created_at->diffForHumans() }}
@else -
{{ $comment->commenter->global_name ?? $comment->commenter->username }} - {{ $comment->created_at->diffForHumans() }}
+
{{ $comment->commenter->name }} - {{ $comment->created_at->diffForHumans() }}
@endif - +
{!! $markdown->line($comment->comment) !!}
@if (! Illuminate\Support\Facades\Route::is('profile.comments')) diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..050e969 --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,24 @@ +@props([ + 'url', + 'color' => 'primary', + 'align' => 'center', +]) + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..c47a260 --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,12 @@ +@props(['url']) + + + +@if (trim($slot) === 'Laravel') + +@else +{!! $slot !!} +@endif + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..0fa6b82 --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,58 @@ + + + +{{ config('app.name') }} + + + + + +{!! $head ?? '' !!} + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..5ebfd90 --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ + +{{-- Header --}} + + + + + + +{{-- Body --}} +{!! $slot !!} + +{{-- Subcopy --}} +@isset($subcopy) + + +{!! $subcopy !!} + + +@endisset + +{{-- Footer --}} + + +© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights not reserved.') }} + + + diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..6e969d1 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,295 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #000; + color: #fff; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3869d4; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #fff; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #000; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #fff; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + max-height: 75px; + width: 267px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #000; + border-bottom: 1px solid #edf2f7; + border-top: 1px solid #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #212121; + border-color: #e8e5ef; + border-radius: 12px; + border-width: 1px; + box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); + margin: 0 auto; + padding: 0; + width: 570px; +} + +.inner-body a { + word-break: break-all; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e8e5ef; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #b0adc5; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #b0adc5; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #edeff2; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + float: unset; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 12px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #be123c; + border-bottom: 8px solid #be123c; + border-left: 18px solid #be123c; + border-right: 18px solid #be123c; + border-top: 8px solid #be123c; +} + +.button-green, +.button-success { + background-color: #48bb78; + border-bottom: 8px solid #48bb78; + border-left: 18px solid #48bb78; + border-right: 18px solid #48bb78; + border-top: 8px solid #48bb78; +} + +.button-red, +.button-error { + background-color: #e53e3e; + border-bottom: 8px solid #e53e3e; + border-left: 18px solid #e53e3e; + border-right: 18px solid #e53e3e; + border-top: 8px solid #e53e3e; +} + +/* Panels */ + +.panel { + border-left: #2d3748 solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #edf2f7; + color: #718096; + padding: 16px; +} + +.panel-content p { + color: #718096; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..ec58e83 --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..80bce21 --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('app.name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/routes/auth.php b/routes/auth.php new file mode 100644 index 0000000..d91e63c --- /dev/null +++ b/routes/auth.php @@ -0,0 +1,64 @@ +group(function () { + Route::post('register', [RegisteredUserController::class, 'store']) + ->name('register'); + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); + + // Discord OAuth (Socialite) + Route::get('/auth/discord', [DiscordAuthController::class, 'redirect']) + ->name('discord.login'); + + Route::get('/auth/discord/callback', [DiscordAuthController::class, 'callback']); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/routes/web.php b/routes/web.php index 6e06847..a22fc08 100644 --- a/routes/web.php +++ b/routes/web.php @@ -63,13 +63,14 @@ Route::middleware('auth')->group(function () { 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'); - Route::post('/user/delete', [UserController::class, 'delete'])->name('profile.delete'); - // 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'); @@ -85,8 +86,6 @@ Route::middleware('auth')->group(function () { Route::get('/download-search', [HomeController::class, 'downloadSearch'])->name('download.search'); }); -Route::get('/user/{username}', [UserController::class, 'index'])->name('user.index'); - /* |--------------------------------------------------------------------------------- | Admin Pages @@ -136,3 +135,5 @@ Route::group(['middleware' => ['auth', 'auth.admin']], function () { 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'; diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 0000000..13dcb7c --- /dev/null +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,54 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } + + public function test_users_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); + } +} diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 0000000..705570b --- /dev/null +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,58 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); + } + + public function test_email_is_not_verified_with_invalid_hash(): void + { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 0000000..ff85721 --- /dev/null +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 0000000..aa50350 --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,73 @@ +get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); + + return true; + }); + } +} diff --git a/tests/Feature/Auth/PasswordUpdateTest.php b/tests/Feature/Auth/PasswordUpdateTest.php new file mode 100644 index 0000000..ca28c6c --- /dev/null +++ b/tests/Feature/Auth/PasswordUpdateTest.php @@ -0,0 +1,51 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); + } + + public function test_correct_password_must_be_provided_to_update_password(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->put('/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrorsIn('updatePassword', 'current_password') + ->assertRedirect('/profile'); + } +} diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..1489d0e --- /dev/null +++ b/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,31 @@ +get('/register'); + + $response->assertStatus(200); + } + + public function test_new_users_can_register(): void + { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); + } +} diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php new file mode 100644 index 0000000..252fdcc --- /dev/null +++ b/tests/Feature/ProfileTest.php @@ -0,0 +1,99 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/profile'); + + $response->assertOk(); + } + + public function test_profile_information_can_be_updated(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + $this->assertSame('Test User', $user->name); + $this->assertSame('test@example.com', $user->email); + $this->assertNull($user->email_verified_at); + } + + public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $this->assertNotNull($user->refresh()->email_verified_at); + } + + public function test_user_can_delete_their_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_to_delete_account(): void + { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/profile') + ->delete('/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrorsIn('userDeletion', 'password') + ->assertRedirect('/profile'); + + $this->assertNotNull($user->fresh()); + } +}