Replace Auth System #3

Merged
w33b merged 19 commits from auth-redo into main 2026-01-09 15:11:37 +00:00
44 changed files with 1314 additions and 203 deletions
Showing only changes of commit 256af435ad - Show all commits

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('home.index', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('home.index', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('home.index', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('home.index', absolute: false))
: view('auth.verify-email');
}
}

View File

@@ -0,0 +1,62 @@
<?php
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;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
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.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('home.index', absolute: false).'?verified=1');
}
}

View File

@@ -3,12 +3,16 @@
namespace App\Http\Controllers;
use App\Models\Episode;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
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\View\View;
use Conner\Tagging\Model\Tag;
@@ -17,7 +21,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 +31,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 +41,26 @@ class ProfileController extends Controller
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->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 +70,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 +80,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 +90,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 +104,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 +132,28 @@ class ProfileController extends Controller
*/
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
// 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]);
Auth::logout();
$user->delete();
$user->forceDelete();
$request->session()->invalidate();
$request->session()->regenerateToken();
cache()->flush();
return Redirect::to('/');
}
}

View File

@@ -3,12 +3,6 @@
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Models\Watched;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
@@ -18,41 +12,11 @@ class UserController extends Controller
public function index(string $username): \Illuminate\View\View
{
$user = User::where('name', $username)
->select('id', 'name', 'discord_name', 'avatar', 'created_at', 'is_patreon')
->select('id', 'name', 'discord_id', 'discord_name', 'discord_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('/');
}
}

View File

@@ -22,7 +22,7 @@ class LoginRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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());
}
}

View File

@@ -11,13 +11,20 @@ class ProfileUpdateRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

View File

@@ -2,6 +2,7 @@
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;
@@ -63,6 +64,23 @@ class User extends Authenticatable
'discord_avatar' => 'string',
];
/**
* Get the user name
*/
public function getUserName(): string
{
if (!$this->discord_name) {
return $this->name;
}
if ($this->discord_name == $this->name)
{
return $this->name;
}
return "{$this->name} ({$this->discord_name})";
}
/**
* Has Many Playlists.
*/

View File

@@ -33,6 +33,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",

63
composer.lock generated
View File

@@ -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": "1759692ca41f87ed3c80648a187d595b",
"content-hash": "ba6aa8b56350d49f92450bc54ed64912",
"packages": [
{
"name": "brick/math",
@@ -8721,6 +8721,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",

36
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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 });

View File

@@ -0,0 +1,27 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,25 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('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.') }}
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,47 @@
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember">
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,52 @@
<x-guest-layout>
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,39 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,31 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('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.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@@ -1 +1 @@
<a {{ $attributes->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 }}</a>
<a {{ $attributes->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 }}</a>

View File

@@ -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
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
@@ -28,11 +20,11 @@ switch ($width) {
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">

View File

@@ -40,6 +40,7 @@ $maxWidth = [
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"

View File

@@ -1,10 +1,9 @@
@props(['active'])
@php
$classes =
$active ?? false
? 'block w-full pl-3 pr-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-left text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-left text-base font-medium text-neutral-600 dark:text-neutral-200 hover:text-neutral-800 dark:hover:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 focus:outline-none focus:text-neutral-800 dark:focus:text-neutral-200 focus:bg-neutral-50 dark:focus:bg-neutral-700 focus:border-neutral-300 dark:focus:border-neutral-600 transition duration-150 ease-in-out';
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 focus:outline-none focus:text-neutral-800 dark:focus:text-neutral-200 focus:bg-neutral-50 dark:focus:bg-neutral-700 focus:border-neutral-300 dark:focus:border-neutral-600 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>

View File

@@ -1,3 +1,3 @@
@props(['disabled' => false])
<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm']) !!}>
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm']) }}>

View File

@@ -97,7 +97,7 @@
@if (Auth::user()->discord_avatar)
<img class="h-8 w-8 rounded-full object-cover mr-2"
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->discord_id }}/{{ Auth::user()->discord_avatar }}.webp"
alt="{{ Auth::user()->getTagAttribute() }}" />
alt="{{ Auth::user()->getUserName() }}" />
@endif
@else
<img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp"
@@ -106,7 +106,7 @@
@auth
<div style="display: flex; flex-direction: row; align-items: flex-start;">
{{ Auth::user()->getTagAttribute() }}
{{ Auth::user()->getUserName() }}
@if ($notAvailable)
<i class="fa-solid fa-bell text-rose-600"></i>
@endif
@@ -234,7 +234,7 @@
@if (Auth::user()->discord_avatar)
<img class="h-8 w-8 rounded-full object-cover mr-2"
src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ Auth::user()->discord_id }}/{{ Auth::user()->discord_avatar }}.webp"
alt="{{ Auth::user()->getTagAttribute() }}" />
alt="{{ Auth::user()->getUserName() }}" />
@else
<img class="h-8 w-8 rounded-full object-cover mr-2" src="/images/default-avatar.webp"
alt="Guest" />

View File

@@ -1,78 +0,0 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="flex flex-row gap-4 flex-wrap">
<!-- Profile Image -->
<div class="p-2 bg-white dark:bg-neutral-800 shadow rounded-lg flex-none">
@if($user->discord_avatar)
<img class="w-28 h-28 rounded-lg m-2" src="https://external-content.duckduckgo.com/iu/?u=https://cdn.discordapp.com/avatars/{{ $user->discord_id }}/{{ $user->discord_avatar }}.webp" alt="{{ $user->discord_name ?? $user->name }} Avatar">
@else
<img class="w-24 h-24 rounded-lg m-2" src="/images/default-avatar.webp" alt="{{ $user->discord_name ?? $user->name }} Avatar">
@endif
</div>
<!-- Joined -->
<div class="p-4 sm:p-8 bg-white dark:bg-neutral-800 shadow rounded-lg content-center text-center grow">
<div class="inline-block rounded-md text-sky-500">
<i class="fa-solid fa-clock text-3xl"></i>
</div>
<h5 class="font-medium dark:text-neutral-300">
Joined {{ $user->created_at->format('Y-m') }}
</h5>
</div>
<!-- View Count -->
<div class="p-4 sm:p-8 bg-white dark:bg-neutral-800 shadow rounded-lg content-center text-center grow">
<a href="{{ route('user.watched') }}">
<div class="inline-block rounded-md text-sky-500">
<i class="fa-solid fa-eye text-3xl"></i>
</div>
<h5 class="font-medium dark:text-neutral-300">
{{ number_format($user->watched->count()) }} views
</h5>
</a>
</div>
<!-- Comment Count -->
<div class="p-4 sm:p-8 bg-white dark:bg-neutral-800 shadow rounded-lg content-center text-center grow">
<a href="{{ route('profile.comments') }}">
<div class="inline-block rounded-md text-sky-500">
<i class="fa-solid fa-comment text-3xl"></i>
</div>
<h5 class="font-medium dark:text-neutral-300">
{{ number_format($user->commentCount()) }} comments
</h5>
</a>
</div>
<!-- Likes -->
<div class="p-4 sm:p-8 bg-white dark:bg-neutral-800 shadow rounded-lg content-center text-center grow">
<a href="{{ route('profile.likes') }}">
<div class="inline-block rounded-md text-sky-500">
<i class="fa-solid fa-heart text-3xl"></i>
</div>
<h5 class="font-medium dark:text-neutral-300">
{{ number_format($user->likes()) }} likes
</h5>
</a>
</div>
<!-- Playlists -->
<div class="p-4 sm:p-8 bg-white dark:bg-neutral-800 shadow rounded-lg content-center text-center grow">
<a href="{{ route('profile.playlists') }}">
<div class="inline-block rounded-md text-sky-500">
<i class="fa-solid fa-rectangle-list text-3xl"></i>
</div>
<h5 class="font-medium dark:text-neutral-300">
{{ number_format($user->playlists->count()) }} playlists
</h5>
</a>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -10,29 +10,43 @@
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="POST" action="{{ route('profile.delete') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted.') }}
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
{{-- <div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div> --}}
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ml-3">
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>

View File

@@ -14,20 +14,20 @@
@method('put')
<div>
<x-input-label for="current_password" :value="__('Current Password')" />
<x-text-input id="current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="password" :value="__('New Password')" />
<x-text-input id="password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>

View File

@@ -1,36 +1,72 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<i class="fa-brands fa-discord"></i> {{ __('Profile Information') }}
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
<div class="mt-6 space-y-6">
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf
</form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
@csrf
@method('patch')
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" disabled />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
@if (Auth::user()->discord_name)
<div>
<x-input-label for="discord_name" :value="__('Display Name')" />
<x-input-label for="discord_name" :value="__('Discord Name')" />
<x-text-input id="discord_name" name="discord_name" type="text" class="mt-1 block w-full" :value="old('discord_name', $user->discord_name)" required autocomplete="discord_name" disabled />
<x-input-error class="mt-2" :messages="$errors->get('discord_name')" />
</div>
@endif
<div>
<x-input-label for="name" :value="__('Username')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autocomplete="name" disabled />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email ?? __('Unknown'))" required autocomplete="email" disabled />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="email" disabled />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->verified)
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
</div>
@endif
</div>
</div>
{{-- <div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600 dark:text-gray-400"
>{{ __('Saved.') }}</p>
@endif
</div> --}}
</form>
</section>

59
routes/auth.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
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');
});
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');
});

View File

@@ -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::post('/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');
@@ -136,3 +137,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';

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
use RefreshDatabase;
public function test_login_screen_can_be_rendered(): void
{
$response = $this->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('/');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
class EmailVerificationTest extends TestCase
{
use RefreshDatabase;
public function test_email_verification_screen_can_be_rendered(): void
{
$user = User::factory()->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());
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PasswordConfirmationTest extends TestCase
{
use RefreshDatabase;
public function test_confirm_password_screen_can_be_rendered(): void
{
$user = User::factory()->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();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class PasswordResetTest extends TestCase
{
use RefreshDatabase;
public function test_reset_password_link_screen_can_be_rendered(): void
{
$response = $this->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;
});
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Auth;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class PasswordUpdateTest extends TestCase
{
use RefreshDatabase;
public function test_password_can_be_updated(): void
{
$user = User::factory()->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');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Auth;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_registration_screen_can_be_rendered(): void
{
$response = $this->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));
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProfileTest extends TestCase
{
use RefreshDatabase;
public function test_profile_page_is_displayed(): void
{
$user = User::factory()->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());
}
}