Install Jetstream
This commit is contained in:
parent
cfae3c1e24
commit
5c606bbbf0
35
app/Actions/Fortify/CreateNewUser.php
Normal file
35
app/Actions/Fortify/CreateNewUser.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => $this->passwordRules(),
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
}
|
||||
}
|
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
18
app/Actions/Fortify/PasswordValidationRules.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
29
app/Actions/Fortify/ResetUserPassword.php
Normal file
29
app/Actions/Fortify/ResetUserPassword.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
32
app/Actions/Fortify/UpdateUserPassword.php
Normal file
32
app/Actions/Fortify/UpdateUserPassword.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
56
app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
56
app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if (isset($input['photo'])) {
|
||||
$user->updateProfilePhoto($input['photo']);
|
||||
}
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
19
app/Actions/Jetstream/DeleteUser.php
Normal file
19
app/Actions/Jetstream/DeleteUser.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Jetstream;
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Contracts\DeletesUsers;
|
||||
|
||||
class DeleteUser implements DeletesUsers
|
||||
{
|
||||
/**
|
||||
* Delete the given user.
|
||||
*/
|
||||
public function delete(User $user): void
|
||||
{
|
||||
$user->deleteProfilePhoto();
|
||||
$user->tokens->each->delete();
|
||||
$user->delete();
|
||||
}
|
||||
}
|
@ -6,16 +6,24 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens;
|
||||
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasFactory;
|
||||
use HasProfilePhoto;
|
||||
use Notifiable;
|
||||
use TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
@ -26,11 +34,22 @@ class User extends Authenticatable
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $appends = [
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
|
46
app/Providers/FortifyServiceProvider.php
Normal file
46
app/Providers/FortifyServiceProvider.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
}
|
||||
}
|
43
app/Providers/JetstreamServiceProvider.php
Normal file
43
app/Providers/JetstreamServiceProvider.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Jetstream\DeleteUser;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class JetstreamServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configurePermissions();
|
||||
|
||||
Jetstream::deleteUsersUsing(DeleteUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the permissions that are available within the application.
|
||||
*/
|
||||
protected function configurePermissions(): void
|
||||
{
|
||||
Jetstream::defaultApiTokenPermissions(['read']);
|
||||
|
||||
Jetstream::permissions([
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
]);
|
||||
}
|
||||
}
|
17
app/View/Components/AppLayout.php
Normal file
17
app/View/Components/AppLayout.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
17
app/View/Components/GuestLayout.php
Normal file
17
app/View/Components/GuestLayout.php
Normal file
@ -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');
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
|
@ -2,4 +2,6 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
App\Providers\JetstreamServiceProvider::class,
|
||||
];
|
||||
|
@ -8,7 +8,10 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^11.31",
|
||||
"laravel/tinker": "^2.9"
|
||||
"laravel/jetstream": "^5.3",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
@ -17,7 +20,8 @@
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"phpunit/phpunit": "^11.0.1"
|
||||
"pestphp/pest": "^3.7",
|
||||
"pestphp/pest-plugin-laravel": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
1564
composer.lock
generated
1564
composer.lock
generated
File diff suppressed because it is too large
Load Diff
159
config/fortify.php
Normal file
159
config/fortify.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which authentication guard Fortify will use while
|
||||
| authenticating users. This value should correspond with one of your
|
||||
| guards that is already present in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => 'web',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Password Broker
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which password broker Fortify can use when a user
|
||||
| is resetting their password. This configured value should match one
|
||||
| of your password brokers setup in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => 'users',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Username / Email
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines which model attribute should be considered as your
|
||||
| application's "username" field. Typically, this might be the email
|
||||
| address of the users but you are free to change this value here.
|
||||
|
|
||||
| Out of the box, Fortify expects forgot password and reset password
|
||||
| requests to have a field named 'email'. If the application uses
|
||||
| another name for the field you may define it below as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'username' => 'email',
|
||||
|
||||
'email' => 'email',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Lowercase Usernames
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines whether usernames should be lowercased before saving
|
||||
| them in the database, as some database system string fields are case
|
||||
| sensitive. You may disable this for your application if necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'lowercase_usernames' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Home Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the path where users will get redirected during
|
||||
| authentication or password reset when the operations are successful
|
||||
| and the user is authenticated. You are free to change this value.
|
||||
|
|
||||
*/
|
||||
|
||||
'home' => '/dashboard',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Routes Prefix / Subdomain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which prefix Fortify will assign to all the routes
|
||||
| that it registers with the application. If necessary, you may change
|
||||
| subdomain under which all of the Fortify routes will be available.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => '',
|
||||
|
||||
'domain' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Routes Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which middleware Fortify will assign to the routes
|
||||
| that it registers with the application. If necessary, you may change
|
||||
| these middleware but typically this provided default is preferred.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rate Limiting
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default, Fortify will throttle logins to five requests per minute for
|
||||
| every email and IP address combination. However, if you would like to
|
||||
| specify a custom rate limiter to call then you may specify it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'limiters' => [
|
||||
'login' => 'login',
|
||||
'two-factor' => 'two-factor',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register View Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify if the routes returning views should be disabled as
|
||||
| you may not need them when building your own application. This may be
|
||||
| especially true if you're writing a custom single-page application.
|
||||
|
|
||||
*/
|
||||
|
||||
'views' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Features
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some of the Fortify features are optional. You may disable the features
|
||||
| by removing them from this array. You're free to only remove some of
|
||||
| these features or you can even remove all of these if you need to.
|
||||
|
|
||||
*/
|
||||
|
||||
'features' => [
|
||||
Features::registration(),
|
||||
Features::resetPasswords(),
|
||||
// Features::emailVerification(),
|
||||
Features::updateProfileInformation(),
|
||||
Features::updatePasswords(),
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
// 'window' => 0,
|
||||
]),
|
||||
],
|
||||
|
||||
];
|
81
config/jetstream.php
Normal file
81
config/jetstream.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Middleware\AuthenticateSession;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Jetstream Stack
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration value informs Jetstream which "stack" you will be
|
||||
| using for your application. In general, this value is set for you
|
||||
| during installation and will not need to be changed after that.
|
||||
|
|
||||
*/
|
||||
|
||||
'stack' => 'livewire',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Jetstream Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which middleware Jetstream will assign to the routes
|
||||
| that it registers with the application. When necessary, you may modify
|
||||
| these middleware; however, this default value is usually sufficient.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
|
||||
'auth_session' => AuthenticateSession::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Jetstream Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the authentication guard Jetstream will use while
|
||||
| authenticating users. This value should correspond with one of your
|
||||
| guards that is already present in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => 'sanctum',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Features
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some of Jetstream's features are optional. You may disable the features
|
||||
| by removing them from this array. You're free to only remove some of
|
||||
| these features or you can even remove all of these if you need to.
|
||||
|
|
||||
*/
|
||||
|
||||
'features' => [
|
||||
// Features::termsAndPrivacyPolicy(),
|
||||
// Features::profilePhotos(),
|
||||
// Features::api(),
|
||||
// Features::teams(['invitations' => true]),
|
||||
Features::accountDeletion(),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Profile Photo Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration value determines the default disk that will be used
|
||||
| when storing profile photos for your application's users. Typically
|
||||
| this will be the "public" disk but you may adjust this if needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'profile_photo_disk' => 'public',
|
||||
|
||||
];
|
83
config/sanctum.php
Normal file
83
config/sanctum.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort()
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
@ -2,9 +2,12 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
@ -28,7 +31,11 @@ public function definition(): array
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'remember_token' => Str::random(10),
|
||||
'profile_photo_path' => null,
|
||||
'current_team_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@ -41,4 +48,25 @@ public function unverified(): static
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the user should have a personal team.
|
||||
*/
|
||||
public function withPersonalTeam(?callable $callback = null): static
|
||||
{
|
||||
if (! Features::hasTeamFeatures()) {
|
||||
return $this->state([]);
|
||||
}
|
||||
|
||||
return $this->has(
|
||||
Team::factory()
|
||||
->state(fn (array $attributes, User $user) => [
|
||||
'name' => $user->name.'\'s Team',
|
||||
'user_id' => $user->id,
|
||||
'personal_team' => true,
|
||||
])
|
||||
->when(is_callable($callback), $callback),
|
||||
'ownedTeams'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ public function up(): void
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->foreignId('current_team_id')->nullable();
|
||||
$table->string('profile_photo_path', 2048)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->text('two_factor_secret')
|
||||
->after('password')
|
||||
->nullable();
|
||||
|
||||
$table->text('two_factor_recovery_codes')
|
||||
->after('two_factor_secret')
|
||||
->nullable();
|
||||
|
||||
if (Fortify::confirmsTwoFactorAuthentication()) {
|
||||
$table->timestamp('two_factor_confirmed_at')
|
||||
->after('two_factor_recovery_codes')
|
||||
->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(array_merge([
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
], Fortify::confirmsTwoFactorAuthentication() ? [
|
||||
'two_factor_confirmed_at',
|
||||
] : []));
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
3060
package-lock.json
generated
Normal file
3060
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,12 +6,14 @@
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"axios": "^1.7.4",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^1.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^6.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
[x-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
3
resources/markdown/policy.md
Normal file
3
resources/markdown/policy.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Privacy Policy
|
||||
|
||||
Edit this file to define the privacy policy for your application.
|
3
resources/markdown/terms.md
Normal file
3
resources/markdown/terms.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Terms of Service
|
||||
|
||||
Edit this file to define the terms of service for your application.
|
169
resources/views/api/api-token-manager.blade.php
Normal file
169
resources/views/api/api-token-manager.blade.php
Normal file
@ -0,0 +1,169 @@
|
||||
<div>
|
||||
<!-- Generate API Token -->
|
||||
<x-form-section submit="createApiToken">
|
||||
<x-slot name="title">
|
||||
{{ __('Create API Token') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
<!-- Token Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="name" value="{{ __('Token Name') }}" />
|
||||
<x-input id="name" type="text" class="mt-1 block w-full" wire:model="createApiTokenForm.name" autofocus />
|
||||
<x-input-error for="name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Token Permissions -->
|
||||
@if (Laravel\Jetstream\Jetstream::hasPermissions())
|
||||
<div class="col-span-6">
|
||||
<x-label for="permissions" value="{{ __('Permissions') }}" />
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach (Laravel\Jetstream\Jetstream::$permissions as $permission)
|
||||
<label class="flex items-center">
|
||||
<x-checkbox wire:model="createApiTokenForm.permissions" :value="$permission"/>
|
||||
<span class="ms-2 text-sm text-gray-600">{{ $permission }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-action-message class="me-3" on="created">
|
||||
{{ __('Created.') }}
|
||||
</x-action-message>
|
||||
|
||||
<x-button>
|
||||
{{ __('Create') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-form-section>
|
||||
|
||||
@if ($this->user->tokens->isNotEmpty())
|
||||
<x-section-border />
|
||||
|
||||
<!-- Manage API Tokens -->
|
||||
<div class="mt-10 sm:mt-0">
|
||||
<x-action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Manage API Tokens') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('You may delete any of your existing tokens if they are no longer needed.') }}
|
||||
</x-slot>
|
||||
|
||||
<!-- API Token List -->
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
@foreach ($this->user->tokens->sortBy('name') as $token)
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="break-all">
|
||||
{{ $token->name }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center ms-2">
|
||||
@if ($token->last_used_at)
|
||||
<div class="text-sm text-gray-400">
|
||||
{{ __('Last used') }} {{ $token->last_used_at->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasPermissions())
|
||||
<button class="cursor-pointer ms-6 text-sm text-gray-400 underline" wire:click="manageApiTokenPermissions({{ $token->id }})">
|
||||
{{ __('Permissions') }}
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button class="cursor-pointer ms-6 text-sm text-red-500" wire:click="confirmApiTokenDeletion({{ $token->id }})">
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-action-section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Token Value Modal -->
|
||||
<x-dialog-modal wire:model.live="displayingToken">
|
||||
<x-slot name="title">
|
||||
{{ __('API Token') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div>
|
||||
{{ __('Please copy your new API token. For your security, it won\'t be shown again.') }}
|
||||
</div>
|
||||
|
||||
<x-input x-ref="plaintextToken" type="text" readonly :value="$plainTextToken"
|
||||
class="mt-4 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500 w-full break-all"
|
||||
autofocus autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
@showing-token-modal.window="setTimeout(() => $refs.plaintextToken.select(), 250)"
|
||||
/>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="$set('displayingToken', false)" wire:loading.attr="disabled">
|
||||
{{ __('Close') }}
|
||||
</x-secondary-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
|
||||
<!-- API Token Permissions Modal -->
|
||||
<x-dialog-modal wire:model.live="managingApiTokenPermissions">
|
||||
<x-slot name="title">
|
||||
{{ __('API Token Permissions') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach (Laravel\Jetstream\Jetstream::$permissions as $permission)
|
||||
<label class="flex items-center">
|
||||
<x-checkbox wire:model="updateApiTokenForm.permissions" :value="$permission"/>
|
||||
<span class="ms-2 text-sm text-gray-600">{{ $permission }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="$set('managingApiTokenPermissions', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-button class="ms-3" wire:click="updateApiToken" wire:loading.attr="disabled">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
|
||||
<!-- Delete Token Confirmation Modal -->
|
||||
<x-confirmation-modal wire:model.live="confirmingApiTokenDeletion">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete API Token') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Are you sure you would like to delete this API token?') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="$toggle('confirmingApiTokenDeletion')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-danger-button class="ms-3" wire:click="deleteApiToken" wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-danger-button>
|
||||
</x-slot>
|
||||
</x-confirmation-modal>
|
||||
</div>
|
13
resources/views/api/index.blade.php
Normal file
13
resources/views/api/index.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('API Tokens') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
@livewire('api.api-token-manager')
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
28
resources/views/auth/confirm-password.blade.php
Normal file
28
resources/views/auth/confirm-password.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
|
||||
</div>
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-label for="password" value="{{ __('Password') }}" />
|
||||
<x-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<x-button class="ms-4">
|
||||
{{ __('Confirm') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
34
resources/views/auth/forgot-password.blade.php
Normal file
34
resources/views/auth/forgot-password.blade.php
Normal file
@ -0,0 +1,34 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('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')
|
||||
<div class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ $value }}
|
||||
</div>
|
||||
@endsession
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<div class="block">
|
||||
<x-label for="email" value="{{ __('Email') }}" />
|
||||
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-button>
|
||||
{{ __('Email Password Reset Link') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
48
resources/views/auth/login.blade.php
Normal file
48
resources/views/auth/login.blade.php
Normal file
@ -0,0 +1,48 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
@session('status')
|
||||
<div class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ $value }}
|
||||
</div>
|
||||
@endsession
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-label for="email" value="{{ __('Email') }}" />
|
||||
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="password" value="{{ __('Password') }}" />
|
||||
<x-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="current-password" />
|
||||
</div>
|
||||
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="flex items-center">
|
||||
<x-checkbox id="remember_me" name="remember" />
|
||||
<span class="ms-2 text-sm text-gray-600">{{ __('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 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-button class="ms-4">
|
||||
{{ __('Log in') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
60
resources/views/auth/register.blade.php
Normal file
60
resources/views/auth/register.blade.php
Normal file
@ -0,0 +1,60 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-label for="name" value="{{ __('Name') }}" />
|
||||
<x-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="email" value="{{ __('Email') }}" />
|
||||
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="password" value="{{ __('Password') }}" />
|
||||
<x-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-input id="password_confirmation" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasTermsAndPrivacyPolicyFeature())
|
||||
<div class="mt-4">
|
||||
<x-label for="terms">
|
||||
<div class="flex items-center">
|
||||
<x-checkbox name="terms" id="terms" required />
|
||||
|
||||
<div class="ms-2">
|
||||
{!! __('I agree to the :terms_of_service and :privacy_policy', [
|
||||
'terms_of_service' => '<a target="_blank" href="'.route('terms.show').'" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">'.__('Terms of Service').'</a>',
|
||||
'privacy_policy' => '<a target="_blank" href="'.route('policy.show').'" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">'.__('Privacy Policy').'</a>',
|
||||
]) !!}
|
||||
</div>
|
||||
</div>
|
||||
</x-label>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
<x-button class="ms-4">
|
||||
{{ __('Register') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
36
resources/views/auth/reset-password.blade.php
Normal file
36
resources/views/auth/reset-password.blade.php
Normal file
@ -0,0 +1,36 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}">
|
||||
@csrf
|
||||
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
|
||||
<div class="block">
|
||||
<x-label for="email" value="{{ __('Email') }}" />
|
||||
<x-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="password" value="{{ __('Password') }}" />
|
||||
<x-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-input id="password_confirmation" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-button>
|
||||
{{ __('Reset Password') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
58
resources/views/auth/two-factor-challenge.blade.php
Normal file
58
resources/views/auth/two-factor-challenge.blade.php
Normal file
@ -0,0 +1,58 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<div x-data="{ recovery: false }">
|
||||
<div class="mb-4 text-sm text-gray-600" x-show="! recovery">
|
||||
{{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600" x-cloak x-show="recovery">
|
||||
{{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }}
|
||||
</div>
|
||||
|
||||
<x-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('two-factor.login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mt-4" x-show="! recovery">
|
||||
<x-label for="code" value="{{ __('Code') }}" />
|
||||
<x-input id="code" class="block mt-1 w-full" type="text" inputmode="numeric" name="code" autofocus x-ref="code" autocomplete="one-time-code" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4" x-cloak x-show="recovery">
|
||||
<x-label for="recovery_code" value="{{ __('Recovery Code') }}" />
|
||||
<x-input id="recovery_code" class="block mt-1 w-full" type="text" name="recovery_code" x-ref="recovery_code" autocomplete="one-time-code" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<button type="button" class="text-sm text-gray-600 hover:text-gray-900 underline cursor-pointer"
|
||||
x-show="! recovery"
|
||||
x-on:click="
|
||||
recovery = true;
|
||||
$nextTick(() => { $refs.recovery_code.focus() })
|
||||
">
|
||||
{{ __('Use a recovery code') }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="text-sm text-gray-600 hover:text-gray-900 underline cursor-pointer"
|
||||
x-cloak
|
||||
x-show="recovery"
|
||||
x-on:click="
|
||||
recovery = false;
|
||||
$nextTick(() => { $refs.code.focus() })
|
||||
">
|
||||
{{ __('Use an authentication code') }}
|
||||
</button>
|
||||
|
||||
<x-button class="ms-4">
|
||||
{{ __('Log in') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
45
resources/views/auth/verify-email.blade.php
Normal file
45
resources/views/auth/verify-email.blade.php
Normal file
@ -0,0 +1,45 @@
|
||||
<x-guest-layout>
|
||||
<x-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
{{ __('Before continuing, 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">
|
||||
{{ __('A new verification link has been sent to the email address you provided in your profile settings.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-button type="submit">
|
||||
{{ __('Resend Verification Email') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="{{ route('profile.show') }}"
|
||||
class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
{{ __('Edit Profile') }}</a>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="inline">
|
||||
@csrf
|
||||
|
||||
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ms-2">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-authentication-card>
|
||||
</x-guest-layout>
|
10
resources/views/components/action-message.blade.php
Normal file
10
resources/views/components/action-message.blade.php
Normal file
@ -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']) }}>
|
||||
{{ $slot->isEmpty() ? 'Saved.' : $slot }}
|
||||
</div>
|
12
resources/views/components/action-section.blade.php
Normal file
12
resources/views/components/action-section.blade.php
Normal file
@ -0,0 +1,12 @@
|
||||
<div {{ $attributes->merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}>
|
||||
<x-section-title>
|
||||
<x-slot name="title">{{ $title }}</x-slot>
|
||||
<x-slot name="description">{{ $description }}</x-slot>
|
||||
</x-section-title>
|
||||
|
||||
<div class="mt-5 md:mt-0 md:col-span-2">
|
||||
<div class="px-4 py-5 sm:p-6 bg-white shadow sm:rounded-lg">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
resources/views/components/application-logo.blade.php
Normal file
5
resources/views/components/application-logo.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 317 48" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M74.09 30.04V13h-4.14v21H82.1v-3.96h-8.01zM95.379 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM106.628 21.58V19h-3.87v15h3.87v-7.17c0-3.15 2.55-4.05 4.56-3.81V18.7c-1.89 0-3.78.84-4.56 2.88zM124.295 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM141.544 19l-3.66 10.5-3.63-10.5h-4.26l5.7 15h4.41l5.7-15h-4.26zM150.354 28.09h11.31c.09-.51.15-1.02.15-1.59 0-4.41-3.15-7.92-7.59-7.92-4.71 0-7.92 3.45-7.92 7.92s3.18 7.92 8.22 7.92c2.88 0 5.13-1.17 6.54-3.21l-3.12-1.8c-.66.87-1.86 1.5-3.36 1.5-2.04 0-3.69-.84-4.23-2.82zm-.06-3c.45-1.92 1.86-3.03 3.93-3.03 1.62 0 3.24.87 3.72 3.03h-7.65zM164.516 34h3.87V12.1h-3.87V34zM185.248 34.36c3.69 0 6.9-2.01 6.9-6.3V13h-2.1v15.06c0 3.03-2.07 4.26-4.8 4.26-2.19 0-3.93-.78-4.62-2.61l-1.77 1.05c1.05 2.43 3.57 3.6 6.39 3.6zM203.124 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM221.224 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM225.176 22.93c0-1.62 1.59-2.37 3.15-2.37 1.44 0 2.97.57 3.6 2.1l1.65-.96c-.87-1.86-2.79-3.06-5.25-3.06-3 0-5.13 1.89-5.13 4.29 0 5.52 8.76 3.39 8.76 7.11 0 1.77-1.68 2.4-3.45 2.4-2.01 0-3.57-.99-4.11-2.52l-1.68.99c.75 1.92 2.79 3.45 5.79 3.45 3.21 0 5.43-1.77 5.43-4.32 0-5.52-8.76-3.39-8.76-7.11zM244.603 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM249.883 21.49V19h-1.98v15h1.98v-8.34c0-3.72 2.34-4.98 4.74-4.98v-1.92c-1.92 0-3.69.63-4.74 2.73zM263.358 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM286.848 19v2.94c-1.26-2.01-3.39-3.3-6.06-3.3-4.23 0-7.74 3.42-7.74 7.86s3.51 7.86 7.74 7.86c2.67 0 4.8-1.29 6.06-3.3V34h1.98V19h-1.98zm-5.91 13.44c-3.33 0-5.91-2.61-5.91-5.94 0-3.33 2.58-5.94 5.91-5.94s5.91 2.61 5.91 5.94c0 3.33-2.58 5.94-5.91 5.94zM309.01 18.64c-1.92 0-3.75.87-4.86 2.73-.84-1.74-2.46-2.73-4.56-2.73-1.8 0-3.42.72-4.59 2.55V19h-1.98v15H295v-8.31c0-3.72 2.16-5.13 4.32-5.13 2.13 0 3.51 1.41 3.51 4.08V34h1.98v-8.31c0-3.72 1.86-5.13 4.17-5.13 2.13 0 3.66 1.41 3.66 4.08V34h1.98v-9.36c0-3.75-2.31-6-5.61-6z" class="fill-black"/>
|
||||
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5"/>
|
||||
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
4
resources/views/components/application-mark.blade.php
Normal file
4
resources/views/components/application-mark.blade.php
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
|
||||
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5"/>
|
||||
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 428 B |
@ -0,0 +1,6 @@
|
||||
<a href="/">
|
||||
<svg class="size-16" viewbox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5"/>
|
||||
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5"/>
|
||||
</svg>
|
||||
</a>
|
9
resources/views/components/authentication-card.blade.php
Normal file
9
resources/views/components/authentication-card.blade.php
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||
<div>
|
||||
{{ $logo }}
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
48
resources/views/components/banner.blade.php
Normal file
48
resources/views/components/banner.blade.php
Normal file
@ -0,0 +1,48 @@
|
||||
@props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')])
|
||||
|
||||
<div x-data="{{ json_encode(['show' => true, 'style' => $style, 'message' => $message]) }}"
|
||||
:class="{ 'bg-indigo-500': style == 'success', 'bg-red-700': style == 'danger', 'bg-yellow-500': style == 'warning', 'bg-gray-500': style != 'success' && style != 'danger' && style != 'warning'}"
|
||||
style="display: none;"
|
||||
x-show="show && message"
|
||||
x-on:banner-message.window="
|
||||
style = event.detail.style;
|
||||
message = event.detail.message;
|
||||
show = true;
|
||||
">
|
||||
<div class="max-w-screen-xl mx-auto py-2 px-3 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between flex-wrap">
|
||||
<div class="w-0 flex-1 flex items-center min-w-0">
|
||||
<span class="flex p-2 rounded-lg" :class="{ 'bg-indigo-600': style == 'success', 'bg-red-600': style == 'danger', 'bg-yellow-600': style == 'warning' }">
|
||||
<svg x-show="style == 'success'" class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<svg x-show="style == 'danger'" class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||
</svg>
|
||||
<svg x-show="style != 'success' && style != 'danger' && style != 'warning'" class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>
|
||||
<svg x-show="style == 'warning'" class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4v.01 0 0 " />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<p class="ms-3 font-medium text-sm text-white truncate" x-text="message"></p>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 sm:ms-3">
|
||||
<button
|
||||
type="button"
|
||||
class="-me-1 flex p-2 rounded-md focus:outline-none sm:-me-2 transition"
|
||||
:class="{ 'hover:bg-indigo-600 focus:bg-indigo-600': style == 'success', 'hover:bg-red-600 focus:bg-red-600': style == 'danger', 'hover:bg-yellow-600 focus:bg-yellow-600': style == 'warning'}"
|
||||
aria-label="Dismiss"
|
||||
x-on:click="show = false">
|
||||
<svg class="size-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
3
resources/views/components/button.blade.php
Normal file
3
resources/views/components/button.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
1
resources/views/components/checkbox.blade.php
Normal file
1
resources/views/components/checkbox.blade.php
Normal file
@ -0,0 +1 @@
|
||||
<input type="checkbox" {!! $attributes->merge(['class' => 'rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500']) !!}>
|
27
resources/views/components/confirmation-modal.blade.php
Normal file
27
resources/views/components/confirmation-modal.blade.php
Normal file
@ -0,0 +1,27 @@
|
||||
@props(['id' => null, 'maxWidth' => null])
|
||||
|
||||
<x-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }}>
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto shrink-0 flex items-center justify-center size-12 rounded-full bg-red-100 sm:mx-0 sm:size-10">
|
||||
<svg class="size-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ms-4 sm:text-start">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{{ $title }}
|
||||
</h3>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-600">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 text-end">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</x-modal>
|
46
resources/views/components/confirms-password.blade.php
Normal file
46
resources/views/components/confirms-password.blade.php
Normal file
@ -0,0 +1,46 @@
|
||||
@props(['title' => __('Confirm Password'), 'content' => __('For your security, please confirm your password to continue.'), 'button' => __('Confirm')])
|
||||
|
||||
@php
|
||||
$confirmableId = md5($attributes->wire('then'));
|
||||
@endphp
|
||||
|
||||
<span
|
||||
{{ $attributes->wire('then') }}
|
||||
x-data
|
||||
x-ref="span"
|
||||
x-on:click="$wire.startConfirmingPassword('{{ $confirmableId }}')"
|
||||
x-on:password-confirmed.window="setTimeout(() => $event.detail.id === '{{ $confirmableId }}' && $refs.span.dispatchEvent(new CustomEvent('then', { bubbles: false })), 250);"
|
||||
>
|
||||
{{ $slot }}
|
||||
</span>
|
||||
|
||||
@once
|
||||
<x-dialog-modal wire:model.live="confirmingPassword">
|
||||
<x-slot name="title">
|
||||
{{ $title }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ $content }}
|
||||
|
||||
<div class="mt-4" x-data="{}" x-on:confirming-password.window="setTimeout(() => $refs.confirmable_password.focus(), 250)">
|
||||
<x-input type="password" class="mt-1 block w-3/4" placeholder="{{ __('Password') }}" autocomplete="current-password"
|
||||
x-ref="confirmable_password"
|
||||
wire:model="confirmablePassword"
|
||||
wire:keydown.enter="confirmPassword" />
|
||||
|
||||
<x-input-error for="confirmable_password" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="stopConfirmingPassword" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-button class="ms-3" dusk="confirm-password-button" wire:click="confirmPassword" wire:loading.attr="disabled">
|
||||
{{ $button }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
@endonce
|
3
resources/views/components/danger-button.blade.php
Normal file
3
resources/views/components/danger-button.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center justify-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 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
17
resources/views/components/dialog-modal.blade.php
Normal file
17
resources/views/components/dialog-modal.blade.php
Normal file
@ -0,0 +1,17 @@
|
||||
@props(['id' => null, 'maxWidth' => null])
|
||||
|
||||
<x-modal :id="$id" :maxWidth="$maxWidth" {{ $attributes }}>
|
||||
<div class="px-6 py-4">
|
||||
<div class="text-lg font-medium text-gray-900">
|
||||
{{ $title }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-600">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 text-end">
|
||||
{{ $footer }}
|
||||
</div>
|
||||
</x-modal>
|
1
resources/views/components/dropdown-link.blade.php
Normal file
1
resources/views/components/dropdown-link.blade.php
Normal file
@ -0,0 +1 @@
|
||||
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
|
37
resources/views/components/dropdown.blade.php
Normal file
37
resources/views/components/dropdown.blade.php
Normal file
@ -0,0 +1,37 @@
|
||||
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white', 'dropdownClasses' => ''])
|
||||
|
||||
@php
|
||||
$alignmentClasses = match ($align) {
|
||||
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
|
||||
'top' => 'origin-top',
|
||||
'none', 'false' => '',
|
||||
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
|
||||
};
|
||||
|
||||
$width = match ($width) {
|
||||
'48' => 'w-48',
|
||||
'60' => 'w-60',
|
||||
default => 'w-48',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="relative" x-data="{ open: false }" @click.away="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="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform 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"
|
||||
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }} {{ $dropdownClasses }}"
|
||||
style="display: none;"
|
||||
@click="open = false">
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
|
||||
{{ $content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
24
resources/views/components/form-section.blade.php
Normal file
24
resources/views/components/form-section.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@props(['submit'])
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}>
|
||||
<x-section-title>
|
||||
<x-slot name="title">{{ $title }}</x-slot>
|
||||
<x-slot name="description">{{ $description }}</x-slot>
|
||||
</x-section-title>
|
||||
|
||||
<div class="mt-5 md:mt-0 md:col-span-2">
|
||||
<form wire:submit="{{ $submit }}">
|
||||
<div class="px-4 py-5 bg-white sm:p-6 shadow {{ isset($actions) ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md' }}">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
{{ $form }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isset($actions))
|
||||
<div class="flex items-center justify-end px-4 py-3 bg-gray-50 text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
5
resources/views/components/input-error.blade.php
Normal file
5
resources/views/components/input-error.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
@props(['for'])
|
||||
|
||||
@error($for)
|
||||
<p {{ $attributes->merge(['class' => 'text-sm text-red-600']) }}>{{ $message }}</p>
|
||||
@enderror
|
3
resources/views/components/input.blade.php
Normal file
3
resources/views/components/input.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
@props(['disabled' => false])
|
||||
|
||||
<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>
|
5
resources/views/components/label.blade.php
Normal file
5
resources/views/components/label.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
@props(['value'])
|
||||
|
||||
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
|
||||
{{ $value ?? $slot }}
|
||||
</label>
|
43
resources/views/components/modal.blade.php
Normal file
43
resources/views/components/modal.blade.php
Normal file
@ -0,0 +1,43 @@
|
||||
@props(['id', 'maxWidth'])
|
||||
|
||||
@php
|
||||
$id = $id ?? md5($attributes->wire('model'));
|
||||
|
||||
$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 ?? '2xl'];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{ show: @entangle($attributes->wire('model')) }"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-show="show"
|
||||
id="{{ $id }}"
|
||||
class="jetstream-modal fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
||||
style="display: 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 opacity-75"></div>
|
||||
</div>
|
||||
|
||||
<div x-show="show" class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
|
||||
x-trap.inert.noscroll="show"
|
||||
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>
|
11
resources/views/components/nav-link.blade.php
Normal file
11
resources/views/components/nav-link.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
@props(['active'])
|
||||
|
||||
@php
|
||||
$classes = ($active ?? false)
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 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 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
11
resources/views/components/responsive-nav-link.blade.php
Normal file
11
resources/views/components/responsive-nav-link.blade.php
Normal file
@ -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 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 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 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
|
||||
@endphp
|
||||
|
||||
<a {{ $attributes->merge(['class' => $classes]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
3
resources/views/components/secondary-button.blade.php
Normal file
3
resources/views/components/secondary-button.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
5
resources/views/components/section-border.blade.php
Normal file
5
resources/views/components/section-border.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="hidden sm:block">
|
||||
<div class="py-8">
|
||||
<div class="border-t border-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
13
resources/views/components/section-title.blade.php
Normal file
13
resources/views/components/section-title.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="md:col-span-1 flex justify-between">
|
||||
<div class="px-4 sm:px-0">
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ $title }}</h3>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{{ $description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 sm:px-0">
|
||||
{{ $aside ?? '' }}
|
||||
</div>
|
||||
</div>
|
21
resources/views/components/switchable-team.blade.php
Normal file
21
resources/views/components/switchable-team.blade.php
Normal file
@ -0,0 +1,21 @@
|
||||
@props(['team', 'component' => 'dropdown-link'])
|
||||
|
||||
<form method="POST" action="{{ route('current-team.update') }}" x-data>
|
||||
@method('PUT')
|
||||
@csrf
|
||||
|
||||
<!-- Hidden Team ID -->
|
||||
<input type="hidden" name="team_id" value="{{ $team->id }}">
|
||||
|
||||
<x-dynamic-component :component="$component" href="#" x-on:click.prevent="$root.submit();">
|
||||
<div class="flex items-center">
|
||||
@if (Auth::user()->isCurrentTeam($team))
|
||||
<svg class="me-2 size-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
@endif
|
||||
|
||||
<div class="truncate">{{ $team->name }}</div>
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
</form>
|
11
resources/views/components/validation-errors.blade.php
Normal file
11
resources/views/components/validation-errors.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
@if ($errors->any())
|
||||
<div {{ $attributes }}>
|
||||
<div class="font-medium text-red-600">{{ __('Whoops! Something went wrong.') }}</div>
|
||||
|
||||
<ul class="mt-3 list-disc list-inside text-sm text-red-600">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
96
resources/views/components/welcome.blade.php
Normal file
96
resources/views/components/welcome.blade.php
Normal file
@ -0,0 +1,96 @@
|
||||
<div class="p-6 lg:p-8 bg-white border-b border-gray-200">
|
||||
<x-application-logo class="block h-12 w-auto" />
|
||||
|
||||
<h1 class="mt-8 text-2xl font-medium text-gray-900">
|
||||
Welcome to your Jetstream application!
|
||||
</h1>
|
||||
|
||||
<p class="mt-6 text-gray-500 leading-relaxed">
|
||||
Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed
|
||||
to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe
|
||||
you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel
|
||||
ecosystem to be a breath of fresh air. We hope you love it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-200 bg-opacity-25 grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 p-6 lg:p-8">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="size-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://laravel.com/docs">Documentation</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laravel has wonderful documentation covering every aspect of the framework. Whether you're new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end.
|
||||
</p>
|
||||
|
||||
<p class="mt-4 text-sm">
|
||||
<a href="https://laravel.com/docs" class="inline-flex items-center font-semibold text-indigo-700">
|
||||
Explore the documentation
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500">
|
||||
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="size-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://laracasts.com">Laracasts</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
|
||||
</p>
|
||||
|
||||
<p class="mt-4 text-sm">
|
||||
<a href="https://laracasts.com" class="inline-flex items-center font-semibold text-indigo-700">
|
||||
Start watching Laracasts
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500">
|
||||
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="size-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
<a href="https://tailwindcss.com/">Tailwind</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that doesn't get in your way. You'll be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="size-6 stroke-gray-400">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
<h2 class="ms-3 text-xl font-semibold text-gray-900">
|
||||
Authentication
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-gray-500 text-sm leading-relaxed">
|
||||
Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you're free to get started with what matters most: building your application.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
15
resources/views/dashboard.blade.php
Normal file
15
resources/views/dashboard.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
||||
<x-welcome />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
23
resources/views/emails/team-invitation.blade.php
Normal file
23
resources/views/emails/team-invitation.blade.php
Normal file
@ -0,0 +1,23 @@
|
||||
@component('mail::message')
|
||||
{{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }}
|
||||
|
||||
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration()))
|
||||
{{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }}
|
||||
|
||||
@component('mail::button', ['url' => route('register')])
|
||||
{{ __('Create Account') }}
|
||||
@endcomponent
|
||||
|
||||
{{ __('If you already have an account, you may accept this invitation by clicking the button below:') }}
|
||||
|
||||
@else
|
||||
{{ __('You may accept this invitation by clicking the button below:') }}
|
||||
@endif
|
||||
|
||||
|
||||
@component('mail::button', ['url' => $acceptUrl])
|
||||
{{ __('Accept Invitation') }}
|
||||
@endcomponent
|
||||
|
||||
{{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }}
|
||||
@endcomponent
|
45
resources/views/layouts/app.blade.php
Normal file
45
resources/views/layouts/app.blade.php
Normal file
@ -0,0 +1,45 @@
|
||||
<!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=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<!-- Styles -->
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<x-banner />
|
||||
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
@livewire('navigation-menu')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@if (isset($header))
|
||||
<header class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
@endif
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@stack('modals')
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
27
resources/views/layouts/guest.blade.php
Normal file
27
resources/views/layouts/guest.blade.php
Normal file
@ -0,0 +1,27 @@
|
||||
<!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=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
|
||||
<!-- Styles -->
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body>
|
||||
<div class="font-sans text-gray-900 antialiased">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
219
resources/views/navigation-menu.blade.php
Normal file
219
resources/views/navigation-menu.blade.php
Normal file
@ -0,0 +1,219 @@
|
||||
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<x-application-mark class="block h-9 w-auto" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<!-- Teams Dropdown -->
|
||||
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
|
||||
<div class="ms-3 relative">
|
||||
<x-dropdown align="right" width="60">
|
||||
<x-slot name="trigger">
|
||||
<span class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
|
||||
{{ Auth::user()->currentTeam->name }}
|
||||
|
||||
<svg class="ms-2 -me-0.5 size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="w-60">
|
||||
<!-- Team Management -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Manage Team') }}
|
||||
</div>
|
||||
|
||||
<!-- Team Settings -->
|
||||
<x-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
|
||||
{{ __('Team Settings') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
|
||||
<x-dropdown-link href="{{ route('teams.create') }}">
|
||||
{{ __('Create New Team') }}
|
||||
</x-dropdown-link>
|
||||
@endcan
|
||||
|
||||
<!-- Team Switcher -->
|
||||
@if (Auth::user()->allTeams()->count() > 1)
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Switch Teams') }}
|
||||
</div>
|
||||
|
||||
@foreach (Auth::user()->allTeams() as $team)
|
||||
<x-switchable-team :team="$team" />
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="ms-3 relative">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<button class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
|
||||
<img class="size-8 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
|
||||
</button>
|
||||
@else
|
||||
<span class="inline-flex rounded-md">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
|
||||
{{ Auth::user()->name }}
|
||||
|
||||
<svg class="ms-2 -me-0.5 size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<!-- Account Management -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Manage Account') }}
|
||||
</div>
|
||||
|
||||
<x-dropdown-link href="{{ route('profile.show') }}">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-dropdown-link href="{{ route('api-tokens.index') }}">
|
||||
{{ __('API Tokens') }}
|
||||
</x-dropdown-link>
|
||||
@endif
|
||||
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link href="{{ route('logout') }}"
|
||||
@click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<svg class="size-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="flex items-center px-4">
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<div class="shrink-0 me-3">
|
||||
<img class="size-10 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<!-- Account Management -->
|
||||
<x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
|
||||
{{ __('API Tokens') }}
|
||||
</x-responsive-nav-link>
|
||||
@endif
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link href="{{ route('logout') }}"
|
||||
@click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
|
||||
<!-- Team Management -->
|
||||
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Manage Team') }}
|
||||
</div>
|
||||
|
||||
<!-- Team Settings -->
|
||||
<x-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
|
||||
{{ __('Team Settings') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
|
||||
<x-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
|
||||
{{ __('Create New Team') }}
|
||||
</x-responsive-nav-link>
|
||||
@endcan
|
||||
|
||||
<!-- Team Switcher -->
|
||||
@if (Auth::user()->allTeams()->count() > 1)
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Switch Teams') }}
|
||||
</div>
|
||||
|
||||
@foreach (Auth::user()->allTeams() as $team)
|
||||
<x-switchable-team :team="$team" component="responsive-nav-link" />
|
||||
@endforeach
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
13
resources/views/policy.blade.php
Normal file
13
resources/views/policy.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<x-guest-layout>
|
||||
<div class="pt-4 bg-gray-100">
|
||||
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
|
||||
<div>
|
||||
<x-authentication-card-logo />
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose">
|
||||
{!! $policy !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-guest-layout>
|
53
resources/views/profile/delete-user-form.blade.php
Normal file
53
resources/views/profile/delete-user-form.blade.php
Normal file
@ -0,0 +1,53 @@
|
||||
<x-action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Delete Account') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Permanently delete your account.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
{{ __('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.') }}
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<x-danger-button wire:click="confirmUserDeletion" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-danger-button>
|
||||
</div>
|
||||
|
||||
<!-- Delete User Confirmation Modal -->
|
||||
<x-dialog-modal wire:model.live="confirmingUserDeletion">
|
||||
<x-slot name="title">
|
||||
{{ __('Delete Account') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Are you sure you want to delete your account? 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.') }}
|
||||
|
||||
<div class="mt-4" x-data="{}" x-on:confirming-delete-user.window="setTimeout(() => $refs.password.focus(), 250)">
|
||||
<x-input type="password" class="mt-1 block w-3/4"
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
wire:model="password"
|
||||
wire:keydown.enter="deleteUser" />
|
||||
|
||||
<x-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-danger-button class="ms-3" wire:click="deleteUser" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-danger-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
</x-slot>
|
||||
</x-action-section>
|
@ -0,0 +1,98 @@
|
||||
<x-action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Browser Sessions') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Manage and log out your active sessions on other browsers and devices.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="max-w-xl text-sm text-gray-600">
|
||||
{{ __('If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.') }}
|
||||
</div>
|
||||
|
||||
@if (count($this->sessions) > 0)
|
||||
<div class="mt-5 space-y-6">
|
||||
<!-- Other Browser Sessions -->
|
||||
@foreach ($this->sessions as $session)
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
@if ($session->agent->isDesktop())
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-8 text-gray-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
|
||||
</svg>
|
||||
@else
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-8 text-gray-500">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="ms-3">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $session->agent->platform() ? $session->agent->platform() : __('Unknown') }} - {{ $session->agent->browser() ? $session->agent->browser() : __('Unknown') }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ $session->ip_address }},
|
||||
|
||||
@if ($session->is_current_device)
|
||||
<span class="text-green-500 font-semibold">{{ __('This device') }}</span>
|
||||
@else
|
||||
{{ __('Last active') }} {{ $session->last_active }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center mt-5">
|
||||
<x-button wire:click="confirmLogout" wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-button>
|
||||
|
||||
<x-action-message class="ms-3" on="loggedOut">
|
||||
{{ __('Done.') }}
|
||||
</x-action-message>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<x-dialog-modal wire:model.live="confirmingLogout">
|
||||
<x-slot name="title">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.') }}
|
||||
|
||||
<div class="mt-4" x-data="{}" x-on:confirming-logout-other-browser-sessions.window="setTimeout(() => $refs.password.focus(), 250)">
|
||||
<x-input type="password" class="mt-1 block w-3/4"
|
||||
autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
wire:model="password"
|
||||
wire:keydown.enter="logoutOtherBrowserSessions" />
|
||||
|
||||
<x-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-secondary-button wire:click="$toggle('confirmingLogout')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
|
||||
<x-button class="ms-3"
|
||||
wire:click="logoutOtherBrowserSessions"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-dialog-modal>
|
||||
</x-slot>
|
||||
</x-action-section>
|
45
resources/views/profile/show.blade.php
Normal file
45
resources/views/profile/show.blade.php
Normal file
@ -0,0 +1,45 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __('Profile') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||
@livewire('profile.update-profile-information-form')
|
||||
|
||||
<x-section-border />
|
||||
@endif
|
||||
|
||||
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.update-password-form')
|
||||
</div>
|
||||
|
||||
<x-section-border />
|
||||
@endif
|
||||
|
||||
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.two-factor-authentication-form')
|
||||
</div>
|
||||
|
||||
<x-section-border />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.logout-other-browser-sessions-form')
|
||||
</div>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures())
|
||||
<x-section-border />
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.delete-user-form')
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
124
resources/views/profile/two-factor-authentication-form.blade.php
Normal file
124
resources/views/profile/two-factor-authentication-form.blade.php
Normal file
@ -0,0 +1,124 @@
|
||||
<x-action-section>
|
||||
<x-slot name="title">
|
||||
{{ __('Two Factor Authentication') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Add additional security to your account using two factor authentication.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
@if ($this->enabled)
|
||||
@if ($showingConfirmation)
|
||||
{{ __('Finish enabling two factor authentication.') }}
|
||||
@else
|
||||
{{ __('You have enabled two factor authentication.') }}
|
||||
@endif
|
||||
@else
|
||||
{{ __('You have not enabled two factor authentication.') }}
|
||||
@endif
|
||||
</h3>
|
||||
|
||||
<div class="mt-3 max-w-xl text-sm text-gray-600">
|
||||
<p>
|
||||
{{ __('When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\'s Google Authenticator application.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($this->enabled)
|
||||
@if ($showingQrCode)
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
@if ($showingConfirmation)
|
||||
{{ __('To finish enabling two factor authentication, scan the following QR code using your phone\'s authenticator application or enter the setup key and provide the generated OTP code.') }}
|
||||
@else
|
||||
{{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application or enter the setup key.') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-2 inline-block bg-white">
|
||||
{!! $this->user->twoFactorQrCodeSvg() !!}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
{{ __('Setup Key') }}: {{ decrypt($this->user->two_factor_secret) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($showingConfirmation)
|
||||
<div class="mt-4">
|
||||
<x-label for="code" value="{{ __('Code') }}" />
|
||||
|
||||
<x-input id="code" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
|
||||
wire:model="code"
|
||||
wire:keydown.enter="confirmTwoFactorAuthentication" />
|
||||
|
||||
<x-input-error for="code" class="mt-2" />
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if ($showingRecoveryCodes)
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<p class="font-semibold">
|
||||
{{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
|
||||
@foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
|
||||
<div>{{ $code }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="mt-5">
|
||||
@if (! $this->enabled)
|
||||
<x-confirms-password wire:then="enableTwoFactorAuthentication">
|
||||
<x-button type="button" wire:loading.attr="disabled">
|
||||
{{ __('Enable') }}
|
||||
</x-button>
|
||||
</x-confirms-password>
|
||||
@else
|
||||
@if ($showingRecoveryCodes)
|
||||
<x-confirms-password wire:then="regenerateRecoveryCodes">
|
||||
<x-secondary-button class="me-3">
|
||||
{{ __('Regenerate Recovery Codes') }}
|
||||
</x-secondary-button>
|
||||
</x-confirms-password>
|
||||
@elseif ($showingConfirmation)
|
||||
<x-confirms-password wire:then="confirmTwoFactorAuthentication">
|
||||
<x-button type="button" class="me-3" wire:loading.attr="disabled">
|
||||
{{ __('Confirm') }}
|
||||
</x-button>
|
||||
</x-confirms-password>
|
||||
@else
|
||||
<x-confirms-password wire:then="showRecoveryCodes">
|
||||
<x-secondary-button class="me-3">
|
||||
{{ __('Show Recovery Codes') }}
|
||||
</x-secondary-button>
|
||||
</x-confirms-password>
|
||||
@endif
|
||||
|
||||
@if ($showingConfirmation)
|
||||
<x-confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-secondary-button wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-secondary-button>
|
||||
</x-confirms-password>
|
||||
@else
|
||||
<x-confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-danger-button wire:loading.attr="disabled">
|
||||
{{ __('Disable') }}
|
||||
</x-danger-button>
|
||||
</x-confirms-password>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-action-section>
|
39
resources/views/profile/update-password-form.blade.php
Normal file
39
resources/views/profile/update-password-form.blade.php
Normal file
@ -0,0 +1,39 @@
|
||||
<x-form-section submit="updatePassword">
|
||||
<x-slot name="title">
|
||||
{{ __('Update Password') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Ensure your account is using a long, random password to stay secure.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="current_password" value="{{ __('Current Password') }}" />
|
||||
<x-input id="current_password" type="password" class="mt-1 block w-full" wire:model="state.current_password" autocomplete="current-password" />
|
||||
<x-input-error for="current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="password" value="{{ __('New Password') }}" />
|
||||
<x-input id="password" type="password" class="mt-1 block w-full" wire:model="state.password" autocomplete="new-password" />
|
||||
<x-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-input id="password_confirmation" type="password" class="mt-1 block w-full" wire:model="state.password_confirmation" autocomplete="new-password" />
|
||||
<x-input-error for="password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-action-message class="me-3" on="saved">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
|
||||
<x-button>
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-form-section>
|
@ -0,0 +1,95 @@
|
||||
<x-form-section submit="updateProfileInformation">
|
||||
<x-slot name="title">
|
||||
{{ __('Profile Information') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
{{ __('Update your account\'s profile information and email address.') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
<!-- Profile Photo -->
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||
<!-- Profile Photo File Input -->
|
||||
<input type="file" id="photo" class="hidden"
|
||||
wire:model.live="photo"
|
||||
x-ref="photo"
|
||||
x-on:change="
|
||||
photoName = $refs.photo.files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
photoPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL($refs.photo.files[0]);
|
||||
" />
|
||||
|
||||
<x-label for="photo" value="{{ __('Photo') }}" />
|
||||
|
||||
<!-- Current Profile Photo -->
|
||||
<div class="mt-2" x-show="! photoPreview">
|
||||
<img src="{{ $this->user->profile_photo_url }}" alt="{{ $this->user->name }}" class="rounded-full size-20 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Profile Photo Preview -->
|
||||
<div class="mt-2" x-show="photoPreview" style="display: none;">
|
||||
<span class="block rounded-full size-20 bg-cover bg-no-repeat bg-center"
|
||||
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-secondary-button class="mt-2 me-2" type="button" x-on:click.prevent="$refs.photo.click()">
|
||||
{{ __('Select A New Photo') }}
|
||||
</x-secondary-button>
|
||||
|
||||
@if ($this->user->profile_photo_path)
|
||||
<x-secondary-button type="button" class="mt-2" wire:click="deleteProfilePhoto">
|
||||
{{ __('Remove Photo') }}
|
||||
</x-secondary-button>
|
||||
@endif
|
||||
|
||||
<x-input-error for="photo" class="mt-2" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="name" value="{{ __('Name') }}" />
|
||||
<x-input id="name" type="text" class="mt-1 block w-full" wire:model="state.name" required autocomplete="name" />
|
||||
<x-input-error for="name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="email" value="{{ __('Email') }}" />
|
||||
<x-input id="email" type="email" class="mt-1 block w-full" wire:model="state.email" required autocomplete="username" />
|
||||
<x-input-error for="email" class="mt-2" />
|
||||
|
||||
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::emailVerification()) && ! $this->user->hasVerifiedEmail())
|
||||
<p class="text-sm mt-2">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<button type="button" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" wire:click.prevent="sendEmailVerification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if ($this->verificationLinkSent)
|
||||
<p class="mt-2 font-medium text-sm text-green-600">
|
||||
{{ __('A new verification link has been sent to your email address.') }}
|
||||
</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-action-message class="me-3" on="saved">
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
|
||||
<x-button wire:loading.attr="disabled" wire:target="photo">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
</x-form-section>
|
13
resources/views/terms.blade.php
Normal file
13
resources/views/terms.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
<x-guest-layout>
|
||||
<div class="pt-4 bg-gray-100">
|
||||
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
|
||||
<div>
|
||||
<x-authentication-card-logo />
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose">
|
||||
{!! $terms !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-guest-layout>
|
8
routes/api.php
Normal file
8
routes/api.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
})->middleware('auth:sanctum');
|
@ -5,3 +5,13 @@
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
Route::middleware([
|
||||
'auth:sanctum',
|
||||
config('jetstream.auth_session'),
|
||||
'verified',
|
||||
])->group(function () {
|
||||
Route::get('/dashboard', function () {
|
||||
return view('dashboard');
|
||||
})->name('dashboard');
|
||||
});
|
||||
|
@ -1,14 +1,16 @@
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import forms from '@tailwindcss/forms';
|
||||
import typography from '@tailwindcss/typography';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./vendor/laravel/jetstream/**/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/**/*.blade.php',
|
||||
'./resources/**/*.js',
|
||||
'./resources/**/*.vue',
|
||||
'./resources/views/**/*.blade.php',
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
@ -16,5 +18,6 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
plugins: [forms, typography],
|
||||
};
|
||||
|
38
tests/Feature/ApiTokenPermissionsTest.php
Normal file
38
tests/Feature/ApiTokenPermissionsTest.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('api token permissions can be updated', function () {
|
||||
if (Features::hasTeamFeatures()) {
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
||||
} else {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
}
|
||||
|
||||
$token = $user->tokens()->create([
|
||||
'name' => 'Test Token',
|
||||
'token' => Str::random(40),
|
||||
'abilities' => ['create', 'read'],
|
||||
]);
|
||||
|
||||
Livewire::test(ApiTokenManager::class)
|
||||
->set(['managingPermissionsFor' => $token])
|
||||
->set(['updateApiTokenForm' => [
|
||||
'permissions' => [
|
||||
'delete',
|
||||
'missing-permission',
|
||||
],
|
||||
]])
|
||||
->call('updateApiToken');
|
||||
|
||||
expect($user->fresh()->tokens->first())
|
||||
->can('delete')->toBeTrue()
|
||||
->can('read')->toBeFalse()
|
||||
->can('missing-permission')->toBeFalse();
|
||||
})->skip(function () {
|
||||
return ! Features::hasApiFeatures();
|
||||
}, 'API support is not enabled.');
|
32
tests/Feature/AuthenticationTest.php
Normal file
32
tests/Feature/AuthenticationTest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
test('login screen can be rendered', function () {
|
||||
$response = $this->get('/login');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('users can authenticate using the login screen', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
});
|
||||
|
||||
test('users cannot authenticate with invalid password', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
14
tests/Feature/BrowserSessionsTest.php
Normal file
14
tests/Feature/BrowserSessionsTest.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('other browser sessions can be logged out', function () {
|
||||
$this->actingAs(User::factory()->create());
|
||||
|
||||
Livewire::test(LogoutOtherBrowserSessionsForm::class)
|
||||
->set('password', 'password')
|
||||
->call('logoutOtherBrowserSessions')
|
||||
->assertSuccessful();
|
||||
});
|
32
tests/Feature/CreateApiTokenTest.php
Normal file
32
tests/Feature/CreateApiTokenTest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('api tokens can be created', function () {
|
||||
if (Features::hasTeamFeatures()) {
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
||||
} else {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
}
|
||||
|
||||
Livewire::test(ApiTokenManager::class)
|
||||
->set(['createApiTokenForm' => [
|
||||
'name' => 'Test Token',
|
||||
'permissions' => [
|
||||
'read',
|
||||
'update',
|
||||
],
|
||||
]])
|
||||
->call('createApiToken');
|
||||
|
||||
expect($user->fresh()->tokens)->toHaveCount(1);
|
||||
expect($user->fresh()->tokens->first())
|
||||
->name->toEqual('Test Token')
|
||||
->can('read')->toBeTrue()
|
||||
->can('delete')->toBeFalse();
|
||||
})->skip(function () {
|
||||
return ! Features::hasApiFeatures();
|
||||
}, 'API support is not enabled.');
|
31
tests/Feature/DeleteAccountTest.php
Normal file
31
tests/Feature/DeleteAccountTest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\DeleteUserForm;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('user accounts can be deleted', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(DeleteUserForm::class)
|
||||
->set('password', 'password')
|
||||
->call('deleteUser');
|
||||
|
||||
expect($user->fresh())->toBeNull();
|
||||
})->skip(function () {
|
||||
return ! Features::hasAccountDeletionFeatures();
|
||||
}, 'Account deletion is not enabled.');
|
||||
|
||||
test('correct password must be provided before account can be deleted', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(DeleteUserForm::class)
|
||||
->set('password', 'wrong-password')
|
||||
->call('deleteUser')
|
||||
->assertHasErrors(['password']);
|
||||
|
||||
expect($user->fresh())->not->toBeNull();
|
||||
})->skip(function () {
|
||||
return ! Features::hasAccountDeletionFeatures();
|
||||
}, 'Account deletion is not enabled.');
|
29
tests/Feature/DeleteApiTokenTest.php
Normal file
29
tests/Feature/DeleteApiTokenTest.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('api tokens can be deleted', function () {
|
||||
if (Features::hasTeamFeatures()) {
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
||||
} else {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
}
|
||||
|
||||
$token = $user->tokens()->create([
|
||||
'name' => 'Test Token',
|
||||
'token' => Str::random(40),
|
||||
'abilities' => ['create', 'read'],
|
||||
]);
|
||||
|
||||
Livewire::test(ApiTokenManager::class)
|
||||
->set(['apiTokenIdBeingDeleted' => $token->id])
|
||||
->call('deleteApiToken');
|
||||
|
||||
expect($user->fresh()->tokens)->toHaveCount(0);
|
||||
})->skip(function () {
|
||||
return ! Features::hasApiFeatures();
|
||||
}, 'API support is not enabled.');
|
60
tests/Feature/EmailVerificationTest.php
Normal file
60
tests/Feature/EmailVerificationTest.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
test('email verification screen can be rendered', function () {
|
||||
$user = User::factory()->withPersonalTeam()->create([
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get('/email/verify');
|
||||
|
||||
$response->assertStatus(200);
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::emailVerification());
|
||||
}, 'Email verification not enabled.');
|
||||
|
||||
test('email can be verified', function () {
|
||||
Event::fake();
|
||||
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
|
||||
$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);
|
||||
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::emailVerification());
|
||||
}, 'Email verification not enabled.');
|
||||
|
||||
test('email can not verified with invalid hash', function () {
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::emailVerification());
|
||||
}, 'Email verification not enabled.');
|
@ -1,19 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
it('returns a successful response', function () {
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
35
tests/Feature/PasswordConfirmationTest.php
Normal file
35
tests/Feature/PasswordConfirmationTest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Features;
|
||||
|
||||
test('confirm password screen can be rendered', function () {
|
||||
$user = Features::hasTeamFeatures()
|
||||
? User::factory()->withPersonalTeam()->create()
|
||||
: User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/user/confirm-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('password can be confirmed', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/user/confirm-password', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('password is not confirmed with invalid password', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/user/confirm-password', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors();
|
||||
});
|
73
tests/Feature/PasswordResetTest.php
Normal file
73
tests/Feature/PasswordResetTest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
test('reset password link screen can be rendered', function () {
|
||||
$response = $this->get('/forgot-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::resetPasswords());
|
||||
}, 'Password updates are not enabled.');
|
||||
|
||||
test('reset password link can be requested', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::resetPasswords());
|
||||
}, 'Password updates are not enabled.');
|
||||
|
||||
test('reset password screen can be rendered', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function (object $notification) {
|
||||
$response = $this->get('/reset-password/'.$notification->token);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
return true;
|
||||
});
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::resetPasswords());
|
||||
}, 'Password updates are not enabled.');
|
||||
|
||||
test('password can be reset with valid token', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function (object $notification) use ($user) {
|
||||
$response = $this->post('/reset-password', [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
return true;
|
||||
});
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::resetPasswords());
|
||||
}, 'Password updates are not enabled.');
|
26
tests/Feature/ProfileInformationTest.php
Normal file
26
tests/Feature/ProfileInformationTest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('current profile information is available', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$component = Livewire::test(UpdateProfileInformationForm::class);
|
||||
|
||||
expect($component->state['name'])->toEqual($user->name);
|
||||
expect($component->state['email'])->toEqual($user->email);
|
||||
});
|
||||
|
||||
test('profile information can be updated', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdateProfileInformationForm::class)
|
||||
->set('state', ['name' => 'Test Name', 'email' => 'test@example.com'])
|
||||
->call('updateProfileInformation');
|
||||
|
||||
expect($user->fresh())
|
||||
->name->toEqual('Test Name')
|
||||
->email->toEqual('test@example.com');
|
||||
});
|
35
tests/Feature/RegistrationTest.php
Normal file
35
tests/Feature/RegistrationTest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
test('registration screen can be rendered', function () {
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(200);
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::registration());
|
||||
}, 'Registration support is not enabled.');
|
||||
|
||||
test('registration screen cannot be rendered if support is disabled', function () {
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(404);
|
||||
})->skip(function () {
|
||||
return Features::enabled(Features::registration());
|
||||
}, 'Registration support is enabled.');
|
||||
|
||||
test('new users can register', function () {
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
})->skip(function () {
|
||||
return ! Features::enabled(Features::registration());
|
||||
}, 'Registration support is not enabled.');
|
58
tests/Feature/TwoFactorAuthenticationSettingsTest.php
Normal file
58
tests/Feature/TwoFactorAuthenticationSettingsTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('two factor authentication can be enabled', function () {
|
||||
$this->actingAs($user = User::factory()->create()->fresh());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
expect($user->two_factor_secret)->not->toBeNull();
|
||||
expect($user->recoveryCodes())->toHaveCount(8);
|
||||
})->skip(function () {
|
||||
return ! Features::canManageTwoFactorAuthentication();
|
||||
}, 'Two factor authentication is not enabled.');
|
||||
|
||||
test('recovery codes can be regenerated', function () {
|
||||
$this->actingAs($user = User::factory()->create()->fresh());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$component = Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication')
|
||||
->call('regenerateRecoveryCodes');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$component->call('regenerateRecoveryCodes');
|
||||
|
||||
expect($user->recoveryCodes())->toHaveCount(8);
|
||||
expect(array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes()))->toHaveCount(8);
|
||||
})->skip(function () {
|
||||
return ! Features::canManageTwoFactorAuthentication();
|
||||
}, 'Two factor authentication is not enabled.');
|
||||
|
||||
test('two factor authentication can be disabled', function () {
|
||||
$this->actingAs($user = User::factory()->create()->fresh());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$component = Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication');
|
||||
|
||||
$this->assertNotNull($user->fresh()->two_factor_secret);
|
||||
|
||||
$component->call('disableTwoFactorAuthentication');
|
||||
|
||||
expect($user->fresh()->two_factor_secret)->toBeNull();
|
||||
})->skip(function () {
|
||||
return ! Features::canManageTwoFactorAuthentication();
|
||||
}, 'Two factor authentication is not enabled.');
|
50
tests/Feature/UpdatePasswordTest.php
Normal file
50
tests/Feature/UpdatePasswordTest.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm;
|
||||
use Livewire\Livewire;
|
||||
|
||||
test('password can be updated', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdatePasswordForm::class)
|
||||
->set('state', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
->call('updatePassword');
|
||||
|
||||
expect(Hash::check('new-password', $user->fresh()->password))->toBeTrue();
|
||||
});
|
||||
|
||||
test('current password must be correct', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdatePasswordForm::class)
|
||||
->set('state', [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
->call('updatePassword')
|
||||
->assertHasErrors(['current_password']);
|
||||
|
||||
expect(Hash::check('password', $user->fresh()->password))->toBeTrue();
|
||||
});
|
||||
|
||||
test('new passwords must match', function () {
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdatePasswordForm::class)
|
||||
->set('state', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'wrong-password',
|
||||
])
|
||||
->call('updatePassword')
|
||||
->assertHasErrors(['password']);
|
||||
|
||||
expect(Hash::check('password', $user->fresh()->password))->toBeTrue();
|
||||
});
|
47
tests/Pest.php
Normal file
47
tests/Pest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
@ -1,16 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
test('that true is true', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user