Replace hand-rolled auth with Laravel Breeze (Livewire stack)
Adds the auth Breeze scaffolds that were missing before: password
reset and email verification, plus a profile page (update name/email,
change password, delete account). User now implements MustVerifyEmail
so verification is actually enforced, not just scaffolded.
This does mean accepting Tailwind v3 (postcss/tailwind.config.js)
over the v4 CSS-first setup the app started with, and Livewire drops
from ^4.3 to ^3.6.4 — both forced by Breeze/Volt's current
compatibility requirements. Restored the Instrument Sans font Breeze's
installer dropped so styling stays close to what we had.
Reconciliation, since Breeze's installer assumes a blank-slate app:
- Deleted the hand-rolled Login/Register Livewire components/views/
test — superseded by Breeze's Volt-based auth pages
(resources/views/livewire/pages/auth/*), which also cover
forgot-password, reset-password, verify-email, and confirm-password.
- Deleted Breeze's own default welcome/dashboard views and its
separate app layout + nav component — kept our existing
components/layouts/app.blade.php as the one real app layout, used
Breeze's minimal layouts/guest.blade.php only for the auth pages.
- Rewrote routes/web.php merging our routes (home, sites, discover)
with routes/auth.php; replaced every hardcoded route('dashboard')
reference (5 Blade files plus, easy to miss, a plain PHP controller
VerifyEmailController) with sites.index.
- Logout: Breeze's Actions\Logout::__invoke() returns void, so wired
directly as a route it silently no-ops instead of redirecting;
fixed with a thin closure that calls it then redirects.
- Fixed Breeze's own generated tests (Auth/AuthenticationTest,
RegistrationTest, EmailVerificationTest, PasswordConfirmationTest)
where they assumed the deleted dashboard route/nav component.
Verified: full suite (28 tests, including Breeze's Registration/
Authentication/PasswordReset/PasswordConfirmation/PasswordUpdate/
EmailVerification/Profile tests plus our own GuestPreviewTest) passes
against the huginn_testing MySQL database. Also smoke-tested for real:
all key pages return 200, /profile redirects guests to /login, guest
preview capping still works, and a real password-reset email was
generated and logged with correct branding and a working link.
This commit is contained in:
@@ -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('sites.index', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()->markEmailAsVerified()) {
|
||||||
|
event(new Verified($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->intended(route('sites.index', absolute: false).'?verified=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Actions;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
|
||||||
|
class Logout
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Log the current user out of the application.
|
||||||
|
*/
|
||||||
|
public function __invoke(): void
|
||||||
|
{
|
||||||
|
Auth::guard('web')->logout();
|
||||||
|
|
||||||
|
Session::invalidate();
|
||||||
|
Session::regenerateToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Auth;
|
|
||||||
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
use Livewire\Attributes\Layout;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
#[Layout('components.layouts.app')]
|
|
||||||
class Login extends Component
|
|
||||||
{
|
|
||||||
public string $email = '';
|
|
||||||
|
|
||||||
public string $password = '';
|
|
||||||
|
|
||||||
public bool $remember = false;
|
|
||||||
|
|
||||||
public function login(): void
|
|
||||||
{
|
|
||||||
$this->validate([
|
|
||||||
'email' => ['required', 'email'],
|
|
||||||
'password' => ['required'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => 'These credentials do not match our records.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
session()->regenerate();
|
|
||||||
|
|
||||||
$this->redirect(route('sites.index'), navigate: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
return view('livewire.auth.login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\Auth;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Contracts\View\View;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Validation\Rules\Password;
|
|
||||||
use Livewire\Attributes\Layout;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
#[Layout('components.layouts.app')]
|
|
||||||
class Register extends Component
|
|
||||||
{
|
|
||||||
public string $name = '';
|
|
||||||
|
|
||||||
public string $email = '';
|
|
||||||
|
|
||||||
public string $password = '';
|
|
||||||
|
|
||||||
public string $password_confirmation = '';
|
|
||||||
|
|
||||||
public function register(): void
|
|
||||||
{
|
|
||||||
$validated = $this->validate([
|
|
||||||
'name' => ['required', 'string', 'max:255'],
|
|
||||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
|
||||||
'password' => ['required', 'confirmed', Password::defaults()],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = User::create([
|
|
||||||
'name' => $validated['name'],
|
|
||||||
'email' => $validated['email'],
|
|
||||||
'password' => $validated['password'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
Auth::login($user);
|
|
||||||
|
|
||||||
session()->regenerate();
|
|
||||||
|
|
||||||
$this->redirect(route('sites.index'), navigate: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
return view('livewire.auth.register');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Forms;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Form;
|
||||||
|
|
||||||
|
class LoginForm extends Form
|
||||||
|
{
|
||||||
|
#[Validate('required|string|email')]
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
#[Validate('required|string')]
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
#[Validate('boolean')]
|
||||||
|
public bool $remember = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to authenticate the request's credentials.
|
||||||
|
*
|
||||||
|
* @throws ValidationException
|
||||||
|
*/
|
||||||
|
public function authenticate(): void
|
||||||
|
{
|
||||||
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
|
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||||
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.failed'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RateLimiter::clear($this->throttleKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the authentication request is not rate limited.
|
||||||
|
*/
|
||||||
|
protected function ensureIsNotRateLimited(): void
|
||||||
|
{
|
||||||
|
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event(new Lockout(request()));
|
||||||
|
|
||||||
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'form.email' => trans('auth.throttle', [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'minutes' => ceil($seconds / 60),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authentication rate limiting throttle key.
|
||||||
|
*/
|
||||||
|
protected function throttleKey(): string
|
||||||
|
{
|
||||||
|
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Database\Factories\UserFactory;
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -12,7 +12,7 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
|
|
||||||
#[Fillable(['name', 'email', 'password'])]
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
#[Hidden(['password', 'remember_token'])]
|
#[Hidden(['password', 'remember_token'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable implements MustVerifyEmail
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
class VoltServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Volt::mount([
|
||||||
|
config('livewire.view_path', resource_path('views/livewire')),
|
||||||
|
resource_path('views/pages'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components;
|
||||||
|
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class GuestLayout extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represents the component.
|
||||||
|
*/
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('layouts.guest');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Providers\AppServiceProvider;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\VoltServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|||||||
+3
-1
@@ -9,10 +9,12 @@
|
|||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"livewire/livewire": "^4.3"
|
"livewire/livewire": "^3.6.4",
|
||||||
|
"livewire/volt": "^1.7.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/breeze": "^2.4",
|
||||||
"laravel/pail": "^1.2.5",
|
"laravel/pail": "^1.2.5",
|
||||||
"laravel/pao": "^1.0.6",
|
"laravel/pao": "^1.0.6",
|
||||||
"laravel/pint": "^1.27",
|
"laravel/pint": "^1.27",
|
||||||
|
|||||||
Generated
+139
-7
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "15d459f8e83fdae144d5144f811f3d68",
|
"content-hash": "9d0f5940f221f92d48c50f0f3d7cb3c9",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -2034,16 +2034,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "livewire/livewire",
|
"name": "livewire/livewire",
|
||||||
"version": "v4.3.3",
|
"version": "v3.8.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/livewire/livewire.git",
|
"url": "https://github.com/livewire/livewire.git",
|
||||||
"reference": "8021f2561865c4c297a3bfca37212a99034377e7"
|
"reference": "e77fce60d0615d68dc6b8fafe98a6739d9752a24"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/8021f2561865c4c297a3bfca37212a99034377e7",
|
"url": "https://api.github.com/repos/livewire/livewire/zipball/e77fce60d0615d68dc6b8fafe98a6739d9752a24",
|
||||||
"reference": "8021f2561865c4c297a3bfca37212a99034377e7",
|
"reference": "e77fce60d0615d68dc6b8fafe98a6739d9752a24",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2098,7 +2098,7 @@
|
|||||||
"description": "A front-end framework for Laravel.",
|
"description": "A front-end framework for Laravel.",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/livewire/livewire/issues",
|
"issues": "https://github.com/livewire/livewire/issues",
|
||||||
"source": "https://github.com/livewire/livewire/tree/v4.3.3"
|
"source": "https://github.com/livewire/livewire/tree/v3.8.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2106,7 +2106,78 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-06-27T03:16:11+00:00"
|
"time": "2026-06-27T03:14:20+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "livewire/volt",
|
||||||
|
"version": "v1.11.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/livewire/volt.git",
|
||||||
|
"reference": "c921edbc4ff2c90e2ca1e87a9e7358e253e33b29"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/livewire/volt/zipball/c921edbc4ff2c90e2ca1e87a9e7358e253e33b29",
|
||||||
|
"reference": "c921edbc4ff2c90e2ca1e87a9e7358e253e33b29",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"laravel/framework": "^10.38.2|^11.0|^12.0|^13.0",
|
||||||
|
"livewire/livewire": "^3.6.1|^4.0",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/folio": "^1.1",
|
||||||
|
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
|
||||||
|
"pestphp/pest": "^2.9.5|^3.0|^4.0",
|
||||||
|
"phpstan/phpstan": "^1.10|^2.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Livewire\\Volt\\VoltServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Livewire\\Volt\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nuno Maduro",
|
||||||
|
"email": "nuno@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An elegantly crafted functional API for Laravel Livewire.",
|
||||||
|
"homepage": "https://github.com/livewire/volt",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"livewire",
|
||||||
|
"volt"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/livewire/volt/issues",
|
||||||
|
"source": "https://github.com/livewire/volt"
|
||||||
|
},
|
||||||
|
"time": "2026-07-22T03:45:31+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
@@ -6323,6 +6394,67 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-04-29T18:32:34+00:00"
|
"time": "2026-04-29T18:32:34+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/breeze",
|
||||||
|
"version": "v2.4.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/breeze.git",
|
||||||
|
"reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/breeze/zipball/4f20e7b2cc8d25daa85d8647241a89c8e0930305",
|
||||||
|
"reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/console": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/filesystem": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/validation": "^11.0|^12.0|^13.0",
|
||||||
|
"php": "^8.2.0",
|
||||||
|
"symfony/console": "^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/framework": "^11.0|^12.0|^13.0",
|
||||||
|
"orchestra/testbench-core": "^9.0|^10.0|^11.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": "2026-05-14T16:54:25+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/pail",
|
"name": "laravel/pail",
|
||||||
"version": "v1.2.7",
|
"version": "v1.2.7",
|
||||||
|
|||||||
Generated
+1078
-6
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -7,10 +7,13 @@
|
|||||||
"dev": "vite"
|
"dev": "vite"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"tailwindcss": "^4.0.0",
|
"postcss": "^8.4.31",
|
||||||
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^8.0.0"
|
"vite": "^8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
@import 'tailwindcss';
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@tailwind utilities;
|
||||||
@source '../../storage/framework/views/*.php';
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
|
||||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@props(['on'])
|
||||||
|
|
||||||
|
<div x-data="{ shown: false, timeout: null }"
|
||||||
|
x-init="@this.on('{{ $on }}', () => { clearTimeout(timeout); shown = true; timeout = setTimeout(() => { shown = false }, 2000); })"
|
||||||
|
x-show.transition.out.opacity.duration.1500ms="shown"
|
||||||
|
x-transition:leave.opacity.duration.1500ms
|
||||||
|
style="display: none;"
|
||||||
|
{{ $attributes->merge(['class' => 'text-sm text-gray-600 dark:text-gray-400']) }}>
|
||||||
|
{{ $slot->isEmpty() ? __('Saved.') : $slot }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@props(['status'])
|
||||||
|
|
||||||
|
@if ($status)
|
||||||
|
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600 dark:text-green-400']) }}>
|
||||||
|
{{ $status }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<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-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$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',
|
||||||
|
};
|
||||||
|
|
||||||
|
$width = match ($width) {
|
||||||
|
'48' => 'w-48',
|
||||||
|
default => $width,
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
|
||||||
|
<div @click="open = ! open">
|
||||||
|
{{ $trigger }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="open"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
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="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">
|
||||||
|
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||||
|
{{ $content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@props(['messages'])
|
||||||
|
|
||||||
|
@if ($messages)
|
||||||
|
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}>
|
||||||
|
@foreach ((array) $messages as $message)
|
||||||
|
<li>{{ $message }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@props(['value'])
|
||||||
|
|
||||||
|
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
|
||||||
|
{{ $value ?? $slot }}
|
||||||
|
</label>
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
<title>{{ isset($title) ? $title.' — ' : '' }}{{ config('app.name', 'Huginn') }}</title>
|
<title>{{ isset($title) ? $title.' — ' : '' }}{{ config('app.name', 'Huginn') }}</title>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
</head>
|
</head>
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<nav class="flex items-center gap-4 text-sm">
|
<nav class="flex items-center gap-4 text-sm">
|
||||||
@auth
|
@auth
|
||||||
<span class="text-neutral-500">{{ auth()->user()->name }}</span>
|
<a href="{{ route('profile') }}" class="text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100">{{ auth()->user()->name }}</a>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100">Log out</button>
|
<button type="submit" class="text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100">Log out</button>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@props([
|
||||||
|
'name',
|
||||||
|
'show' => false,
|
||||||
|
'maxWidth' => '2xl'
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$maxWidth = [
|
||||||
|
'sm' => 'sm:max-w-sm',
|
||||||
|
'md' => 'sm:max-w-md',
|
||||||
|
'lg' => 'sm:max-w-lg',
|
||||||
|
'xl' => 'sm:max-w-xl',
|
||||||
|
'2xl' => 'sm:max-w-2xl',
|
||||||
|
][$maxWidth];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
show: @js($show),
|
||||||
|
focusables() {
|
||||||
|
// All focusable element types...
|
||||||
|
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||||
|
return [...$el.querySelectorAll(selector)]
|
||||||
|
// All non-disabled elements...
|
||||||
|
.filter(el => ! el.hasAttribute('disabled'))
|
||||||
|
},
|
||||||
|
firstFocusable() { return this.focusables()[0] },
|
||||||
|
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||||
|
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||||
|
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||||
|
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||||
|
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||||
|
}"
|
||||||
|
x-init="$watch('show', value => {
|
||||||
|
if (value) {
|
||||||
|
document.body.classList.add('overflow-y-hidden');
|
||||||
|
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('overflow-y-hidden');
|
||||||
|
}
|
||||||
|
})"
|
||||||
|
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()"
|
||||||
|
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||||
|
x-show="show"
|
||||||
|
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||||
|
style="display: {{ $show ? 'block' : 'none' }};"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
class="fixed inset-0 transform transition-all"
|
||||||
|
x-on:click="show = false"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@props(['active'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$classes = ($active ?? false)
|
||||||
|
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||||
|
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@props(['active'])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$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-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@props(['disabled' => false])
|
||||||
|
|
||||||
|
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) }}>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||||
|
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="font-sans text-gray-900 antialiased">
|
||||||
|
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div>
|
||||||
|
<a href="/" wire:navigate class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Huginn</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<div class="mx-auto max-w-sm px-4 py-16">
|
|
||||||
<h1 class="text-2xl font-semibold">Log in</h1>
|
|
||||||
|
|
||||||
<form wire:submit="login" class="mt-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
wire:model="email"
|
|
||||||
autofocus
|
|
||||||
autocomplete="username"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
@error('email') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
wire:model="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
@error('password') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
<input type="checkbox" wire:model="remember" class="rounded border-neutral-300 dark:border-neutral-700">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200"
|
|
||||||
>
|
|
||||||
Log in
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="mt-6 text-sm text-neutral-500">
|
|
||||||
Don't have an account? <a href="{{ route('register') }}" class="text-blue-600 hover:underline dark:text-blue-400">Sign up</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<div class="mx-auto max-w-sm px-4 py-16">
|
|
||||||
<h1 class="text-2xl font-semibold">Create an account</h1>
|
|
||||||
|
|
||||||
<form wire:submit="register" class="mt-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="name" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
wire:model="name"
|
|
||||||
autofocus
|
|
||||||
autocomplete="name"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
@error('name') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
wire:model="email"
|
|
||||||
autocomplete="username"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
@error('email') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
wire:model="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
@error('password') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password_confirmation" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">Confirm password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password_confirmation"
|
|
||||||
wire:model="password_confirmation"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-700 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200"
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="mt-6 text-sm text-neutral-500">
|
|
||||||
Already have an account? <a href="{{ route('login') }}" class="text-blue-600 hover:underline dark:text-blue-400">Log in</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the current user's password.
|
||||||
|
*/
|
||||||
|
public function confirmPassword(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! Auth::guard('web')->validate([
|
||||||
|
'email' => Auth::user()->email,
|
||||||
|
'password' => $this->password,
|
||||||
|
])) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => __('auth.password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
session(['auth.password_confirmed_at' => time()]);
|
||||||
|
|
||||||
|
$this->redirectIntended(default: route('sites.index', absolute: false), navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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 wire:submit="confirmPassword">
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input wire:model="password"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a password reset link to the provided email address.
|
||||||
|
*/
|
||||||
|
public function sendPasswordResetLink(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'email' => ['required', 'string', '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(
|
||||||
|
$this->only('email')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($status != Password::RESET_LINK_SENT) {
|
||||||
|
$this->addError('email', __($status));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reset('email');
|
||||||
|
|
||||||
|
session()->flash('status', __($status));
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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 wire:submit="sendPasswordResetLink">
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input wire:model="email" id="email" class="block mt-1 w-full" type="email" name="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>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Forms\LoginForm;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
public LoginForm $form;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming authentication request.
|
||||||
|
*/
|
||||||
|
public function login(): void
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
$this->form->authenticate();
|
||||||
|
|
||||||
|
Session::regenerate();
|
||||||
|
|
||||||
|
$this->redirectIntended(default: route('sites.index', absolute: false), navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Session Status -->
|
||||||
|
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||||
|
|
||||||
|
<form wire:submit="login">
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input wire:model="form.email" id="email" class="block mt-1 w-full" type="email" name="email" required autofocus autocomplete="username" />
|
||||||
|
<x-input-error :messages="$errors->get('form.email')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="password" :value="__('Password')" />
|
||||||
|
|
||||||
|
<x-text-input wire:model="form.password" id="password" class="block mt-1 w-full"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required autocomplete="current-password" />
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->get('form.password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remember Me -->
|
||||||
|
<div class="block mt-4">
|
||||||
|
<label for="remember" class="inline-flex items-center">
|
||||||
|
<input wire:model="form.remember" id="remember" 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') }}" wire:navigate>
|
||||||
|
{{ __('Forgot your password?') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-primary-button class="ms-3">
|
||||||
|
{{ __('Log in') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Events\Registered;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
public string $name = '';
|
||||||
|
public string $email = '';
|
||||||
|
public string $password = '';
|
||||||
|
public string $password_confirmation = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming registration request.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
|
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validated['password'] = Hash::make($validated['password']);
|
||||||
|
|
||||||
|
event(new Registered($user = User::create($validated)));
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
$this->redirect(route('sites.index', absolute: false), navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form wire:submit="register">
|
||||||
|
<!-- Name -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="name" :value="__('Name')" />
|
||||||
|
<x-text-input wire:model="name" id="name" class="block mt-1 w-full" type="text" name="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 wire:model="email" id="email" class="block mt-1 w-full" type="email" name="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 wire:model="password" 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 wire:model="password_confirmation" 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') }}" wire:navigate>
|
||||||
|
{{ __('Already registered?') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<x-primary-button class="ms-4">
|
||||||
|
{{ __('Register') }}
|
||||||
|
</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
#[Locked]
|
||||||
|
public string $token = '';
|
||||||
|
public string $email = '';
|
||||||
|
public string $password = '';
|
||||||
|
public string $password_confirmation = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount the component.
|
||||||
|
*/
|
||||||
|
public function mount(string $token): void
|
||||||
|
{
|
||||||
|
$this->token = $token;
|
||||||
|
|
||||||
|
$this->email = request()->string('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the password for the given user.
|
||||||
|
*/
|
||||||
|
public function resetPassword(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'token' => ['required'],
|
||||||
|
'email' => ['required', 'string', 'email'],
|
||||||
|
'password' => ['required', 'string', '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(
|
||||||
|
$this->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
|
function ($user) {
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => Hash::make($this->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.
|
||||||
|
if ($status != Password::PASSWORD_RESET) {
|
||||||
|
$this->addError('email', __($status));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::flash('status', __($status));
|
||||||
|
|
||||||
|
$this->redirectRoute('login', navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form wire:submit="resetPassword">
|
||||||
|
<!-- Email Address -->
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input wire:model="email" id="email" class="block mt-1 w-full" type="email" name="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 wire:model="password" 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 wire:model="password_confirmation" 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>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Actions\Logout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new #[Layout('layouts.guest')] class extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send an email verification notification to the user.
|
||||||
|
*/
|
||||||
|
public function sendVerification(): void
|
||||||
|
{
|
||||||
|
if (Auth::user()->hasVerifiedEmail()) {
|
||||||
|
$this->redirectIntended(default: route('sites.index', absolute: false), navigate: true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth::user()->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
Session::flash('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the current user out of the application.
|
||||||
|
*/
|
||||||
|
public function logout(Logout $logout): void
|
||||||
|
{
|
||||||
|
$logout();
|
||||||
|
|
||||||
|
$this->redirect('/', navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
<x-primary-button wire:click="sendVerification">
|
||||||
|
{{ __('Resend Verification Email') }}
|
||||||
|
</x-primary-button>
|
||||||
|
|
||||||
|
<button wire:click="logout" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Actions\Logout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public string $password = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the currently authenticated user.
|
||||||
|
*/
|
||||||
|
public function deleteUser(Logout $logout): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'password' => ['required', 'string', 'current_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
tap(Auth::user(), $logout(...))->delete();
|
||||||
|
|
||||||
|
$this->redirect('/', navigate: true);
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<section class="space-y-6">
|
||||||
|
<header>
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('Delete 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. Before deleting your account, please download any data or information that you wish to retain.') }}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<x-danger-button
|
||||||
|
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->isNotEmpty()" focusable>
|
||||||
|
<form wire:submit="deleteUser" class="p-6">
|
||||||
|
|
||||||
|
<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. 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
|
||||||
|
wire:model="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
class="mt-1 block w-3/4"
|
||||||
|
placeholder="{{ __('Password') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-input-error :messages="$errors->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="ms-3">
|
||||||
|
{{ __('Delete Account') }}
|
||||||
|
</x-danger-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</x-modal>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public string $current_password = '';
|
||||||
|
public string $password = '';
|
||||||
|
public string $password_confirmation = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the password for the currently authenticated user.
|
||||||
|
*/
|
||||||
|
public function updatePassword(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $this->validate([
|
||||||
|
'current_password' => ['required', 'string', 'current_password'],
|
||||||
|
'password' => ['required', 'string', Password::defaults(), 'confirmed'],
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
$this->reset('current_password', 'password', 'password_confirmation');
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth::user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->reset('current_password', 'password', 'password_confirmation');
|
||||||
|
|
||||||
|
$this->dispatch('password-updated');
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('Update Password') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('Ensure your account is using a long, random password to stay secure.') }}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form wire:submit="updatePassword" class="mt-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
|
||||||
|
<x-text-input wire:model="current_password" id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
|
||||||
|
<x-input-error :messages="$errors->get('current_password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="update_password_password" :value="__('New Password')" />
|
||||||
|
<x-text-input wire:model="password" id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||||
|
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
|
||||||
|
<x-text-input wire:model="password_confirmation" id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
|
||||||
|
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||||
|
|
||||||
|
<x-action-message class="me-3" on="password-updated">
|
||||||
|
{{ __('Saved.') }}
|
||||||
|
</x-action-message>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
|
||||||
|
new class extends Component
|
||||||
|
{
|
||||||
|
public string $name = '';
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount the component.
|
||||||
|
*/
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->name = Auth::user()->name;
|
||||||
|
$this->email = Auth::user()->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the profile information for the currently authenticated user.
|
||||||
|
*/
|
||||||
|
public function updateProfileInformation(): void
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$validated = $this->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->fill($validated);
|
||||||
|
|
||||||
|
if ($user->isDirty('email')) {
|
||||||
|
$user->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->dispatch('profile-updated', name: $user->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an email verification notification to the current user.
|
||||||
|
*/
|
||||||
|
public function sendVerification(): void
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->hasVerifiedEmail()) {
|
||||||
|
$this->redirectIntended(default: route('sites.index', absolute: false));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->sendEmailVerificationNotification();
|
||||||
|
|
||||||
|
Session::flash('status', 'verification-link-sent');
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<header>
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ __('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>
|
||||||
|
|
||||||
|
<form wire:submit="updateProfileInformation" class="mt-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<x-input-label for="name" :value="__('Name')" />
|
||||||
|
<x-text-input wire:model="name" id="name" name="name" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" />
|
||||||
|
<x-input-error class="mt-2" :messages="$errors->get('name')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="email" :value="__('Email')" />
|
||||||
|
<x-text-input wire:model="email" id="email" name="email" type="email" class="mt-1 block w-full" required autocomplete="username" />
|
||||||
|
<x-input-error class="mt-2" :messages="$errors->get('email')" />
|
||||||
|
|
||||||
|
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! auth()->user()->hasVerifiedEmail())
|
||||||
|
<div>
|
||||||
|
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
||||||
|
{{ __('Your email address is unverified.') }}
|
||||||
|
|
||||||
|
<button wire:click.prevent="sendVerification" 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 class="flex items-center gap-4">
|
||||||
|
<x-primary-button>{{ __('Save') }}</x-primary-button>
|
||||||
|
|
||||||
|
<x-action-message class="me-3" on="profile-updated">
|
||||||
|
{{ __('Saved.') }}
|
||||||
|
</x-action-message>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<x-layouts.app title="Profile">
|
||||||
|
<div class="mx-auto max-w-3xl space-y-6 px-4 py-8">
|
||||||
|
<h1 class="text-2xl font-semibold">Profile</h1>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900 sm:p-8">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<livewire:profile.update-profile-information-form />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900 sm:p-8">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<livewire:profile.update-password-form />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900 sm:p-8">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<livewire:profile.delete-user-form />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-layouts.app>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
Route::middleware('guest')->group(function () {
|
||||||
|
Volt::route('register', 'pages.auth.register')
|
||||||
|
->name('register');
|
||||||
|
|
||||||
|
Volt::route('login', 'pages.auth.login')
|
||||||
|
->name('login');
|
||||||
|
|
||||||
|
Volt::route('forgot-password', 'pages.auth.forgot-password')
|
||||||
|
->name('password.request');
|
||||||
|
|
||||||
|
Volt::route('reset-password/{token}', 'pages.auth.reset-password')
|
||||||
|
->name('password.reset');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->group(function () {
|
||||||
|
Volt::route('verify-email', 'pages.auth.verify-email')
|
||||||
|
->name('verification.notice');
|
||||||
|
|
||||||
|
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
|
||||||
|
->middleware(['signed', 'throttle:6,1'])
|
||||||
|
->name('verification.verify');
|
||||||
|
|
||||||
|
Volt::route('confirm-password', 'pages.auth.confirm-password')
|
||||||
|
->name('password.confirm');
|
||||||
|
});
|
||||||
+9
-12
@@ -1,11 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\SiteController;
|
use App\Http\Controllers\SiteController;
|
||||||
use App\Livewire\Auth\Login;
|
use App\Livewire\Actions\Logout;
|
||||||
use App\Livewire\Auth\Register;
|
|
||||||
use App\Livewire\Discover\Search as DiscoverSearch;
|
use App\Livewire\Discover\Search as DiscoverSearch;
|
||||||
use App\Livewire\Sites\Index as SitesIndex;
|
use App\Livewire\Sites\Index as SitesIndex;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::view('/', 'home')->name('home');
|
Route::view('/', 'home')->name('home');
|
||||||
@@ -13,15 +11,14 @@ Route::get('/sites', SitesIndex::class)->name('sites.index');
|
|||||||
Route::get('/sites/{site}', [SiteController::class, 'show'])->name('sites.show');
|
Route::get('/sites/{site}', [SiteController::class, 'show'])->name('sites.show');
|
||||||
Route::get('/discover', DiscoverSearch::class)->name('discover');
|
Route::get('/discover', DiscoverSearch::class)->name('discover');
|
||||||
|
|
||||||
Route::middleware('guest')->group(function () {
|
Route::view('profile', 'profile')
|
||||||
Route::get('/login', Login::class)->name('login');
|
->middleware(['auth'])
|
||||||
Route::get('/register', Register::class)->name('register');
|
->name('profile');
|
||||||
});
|
|
||||||
|
|
||||||
Route::post('/logout', function () {
|
Route::post('/logout', function (Logout $logout) {
|
||||||
Auth::logout();
|
$logout();
|
||||||
request()->session()->invalidate();
|
|
||||||
request()->session()->regenerateToken();
|
|
||||||
|
|
||||||
return redirect()->route('home');
|
return redirect('/');
|
||||||
})->middleware('auth')->name('logout');
|
})->middleware('auth')->name('logout');
|
||||||
|
|
||||||
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||||
|
import forms from '@tailwindcss/forms';
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||||
|
'./storage/framework/views/*.php',
|
||||||
|
'./resources/views/**/*.blade.php',
|
||||||
|
],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Instrument Sans', ...defaultTheme.fontFamily.sans],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [forms],
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AuthenticationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_login_screen_can_be_rendered(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/login');
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeVolt('pages.auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_users_can_authenticate_using_the_login_screen(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$component = Volt::test('pages.auth.login')
|
||||||
|
->set('form.email', $user->email)
|
||||||
|
->set('form.password', 'password');
|
||||||
|
|
||||||
|
$component->call('login');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertRedirect(route('sites.index', absolute: false));
|
||||||
|
|
||||||
|
$this->assertAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$component = Volt::test('pages.auth.login')
|
||||||
|
->set('form.email', $user->email)
|
||||||
|
->set('form.password', 'wrong-password');
|
||||||
|
|
||||||
|
$component->call('login');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasErrors()
|
||||||
|
->assertNoRedirect();
|
||||||
|
|
||||||
|
$this->assertGuest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_navigation_shows_the_authenticated_user(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get(route('sites.index'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSeeText($user->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_users_can_logout(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('logout'))
|
||||||
|
->assertRedirect('/');
|
||||||
|
|
||||||
|
$this->assertGuest();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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
|
||||||
|
->assertSeeVolt('pages.auth.verify-email')
|
||||||
|
->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('sites.index', 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
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
|
||||||
|
->assertSeeVolt('pages.auth.confirm-password')
|
||||||
|
->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_password_can_be_confirmed(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('pages.auth.confirm-password')
|
||||||
|
->set('password', 'password');
|
||||||
|
|
||||||
|
$component->call('confirmPassword');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertRedirect('/sites')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_password_is_not_confirmed_with_invalid_password(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('pages.auth.confirm-password')
|
||||||
|
->set('password', 'wrong-password');
|
||||||
|
|
||||||
|
$component->call('confirmPassword');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertNoRedirect()
|
||||||
|
->assertHasErrors('password');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?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 Livewire\Volt\Volt;
|
||||||
|
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
|
||||||
|
->assertSeeVolt('pages.auth.forgot-password')
|
||||||
|
->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reset_password_link_can_be_requested(): void
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Volt::test('pages.auth.forgot-password')
|
||||||
|
->set('email', $user->email)
|
||||||
|
->call('sendPasswordResetLink');
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, ResetPassword::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reset_password_screen_can_be_rendered(): void
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Volt::test('pages.auth.forgot-password')
|
||||||
|
->set('email', $user->email)
|
||||||
|
->call('sendPasswordResetLink');
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||||
|
$response = $this->get('/reset-password/'.$notification->token);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertSeeVolt('pages.auth.reset-password')
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_password_can_be_reset_with_valid_token(): void
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Volt::test('pages.auth.forgot-password')
|
||||||
|
->set('email', $user->email)
|
||||||
|
->call('sendPasswordResetLink');
|
||||||
|
|
||||||
|
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||||
|
$component = Volt::test('pages.auth.reset-password', ['token' => $notification->token])
|
||||||
|
->set('email', $user->email)
|
||||||
|
->set('password', 'password')
|
||||||
|
->set('password_confirmation', 'password');
|
||||||
|
|
||||||
|
$component->call('resetPassword');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertRedirect('/login')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PasswordUpdateTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_password_can_be_updated(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.update-password-form')
|
||||||
|
->set('current_password', 'password')
|
||||||
|
->set('password', 'new-password')
|
||||||
|
->set('password_confirmation', 'new-password')
|
||||||
|
->call('updatePassword');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertNoRedirect();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.update-password-form')
|
||||||
|
->set('current_password', 'wrong-password')
|
||||||
|
->set('password', 'new-password')
|
||||||
|
->set('password_confirmation', 'new-password')
|
||||||
|
->call('updatePassword');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasErrors(['current_password'])
|
||||||
|
->assertNoRedirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class RegistrationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_registration_screen_can_be_rendered(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/register');
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeVolt('pages.auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_new_users_can_register(): void
|
||||||
|
{
|
||||||
|
$component = Volt::test('pages.auth.register')
|
||||||
|
->set('name', 'Test User')
|
||||||
|
->set('email', 'test@example.com')
|
||||||
|
->set('password', 'password')
|
||||||
|
->set('password_confirmation', 'password');
|
||||||
|
|
||||||
|
$component->call('register');
|
||||||
|
|
||||||
|
$component->assertRedirect(route('sites.index', absolute: false));
|
||||||
|
|
||||||
|
$this->assertAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Livewire\Auth\Login;
|
|
||||||
use App\Livewire\Auth\Register;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Livewire\Livewire;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AuthFlowTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_a_user_can_register(): void
|
|
||||||
{
|
|
||||||
Livewire::test(Register::class)
|
|
||||||
->set('name', 'Test User')
|
|
||||||
->set('email', 'testuser@example.com')
|
|
||||||
->set('password', 'correct-horse-battery-staple')
|
|
||||||
->set('password_confirmation', 'correct-horse-battery-staple')
|
|
||||||
->call('register')
|
|
||||||
->assertRedirect(route('sites.index'));
|
|
||||||
|
|
||||||
$this->assertAuthenticated();
|
|
||||||
$this->assertDatabaseHas('users', ['email' => 'testuser@example.com']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_a_user_can_log_in(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create(['password' => 'correct-horse-battery-staple']);
|
|
||||||
|
|
||||||
Livewire::test(Login::class)
|
|
||||||
->set('email', $user->email)
|
|
||||||
->set('password', 'correct-horse-battery-staple')
|
|
||||||
->call('login')
|
|
||||||
->assertRedirect(route('sites.index'));
|
|
||||||
|
|
||||||
$this->assertAuthenticatedAs($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_login_fails_with_wrong_password(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create(['password' => 'correct-horse-battery-staple']);
|
|
||||||
|
|
||||||
Livewire::test(Login::class)
|
|
||||||
->set('email', $user->email)
|
|
||||||
->set('password', 'wrong-password')
|
|
||||||
->call('login')
|
|
||||||
->assertHasErrors('email');
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_a_user_can_log_out(): void
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
|
|
||||||
$this->actingAs($user)
|
|
||||||
->post(route('logout'))
|
|
||||||
->assertRedirect(route('home'));
|
|
||||||
|
|
||||||
$this->assertGuest();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
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()
|
||||||
|
->assertSeeVolt('profile.update-profile-information-form')
|
||||||
|
->assertSeeVolt('profile.update-password-form')
|
||||||
|
->assertSeeVolt('profile.delete-user-form');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_profile_information_can_be_updated(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.update-profile-information-form')
|
||||||
|
->set('name', 'Test User')
|
||||||
|
->set('email', 'test@example.com')
|
||||||
|
->call('updateProfileInformation');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertNoRedirect();
|
||||||
|
|
||||||
|
$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();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.update-profile-information-form')
|
||||||
|
->set('name', 'Test User')
|
||||||
|
->set('email', $user->email)
|
||||||
|
->call('updateProfileInformation');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertNoRedirect();
|
||||||
|
|
||||||
|
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_user_can_delete_their_account(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.delete-user-form')
|
||||||
|
->set('password', 'password')
|
||||||
|
->call('deleteUser');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasNoErrors()
|
||||||
|
->assertRedirect('/');
|
||||||
|
|
||||||
|
$this->assertGuest();
|
||||||
|
$this->assertNull($user->fresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_correct_password_must_be_provided_to_delete_account(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$component = Volt::test('profile.delete-user-form')
|
||||||
|
->set('password', 'wrong-password')
|
||||||
|
->call('deleteUser');
|
||||||
|
|
||||||
|
$component
|
||||||
|
->assertHasErrors('password')
|
||||||
|
->assertNoRedirect();
|
||||||
|
|
||||||
|
$this->assertNotNull($user->fresh());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,11 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import laravel from 'laravel-vite-plugin';
|
import laravel from 'laravel-vite-plugin';
|
||||||
import { bunny } from 'laravel-vite-plugin/fonts';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
fonts: [
|
|
||||||
bunny('Instrument Sans', {
|
|
||||||
weights: [400, 500, 600],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
tailwindcss(),
|
|
||||||
],
|
],
|
||||||
server: {
|
|
||||||
watch: {
|
|
||||||
ignored: ['**/storage/framework/views/**'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user