add Tailwind CSS configuration, privacy policy, terms of service, and dashboard layout; update session driver to database
parent
5a67c1fd72
commit
96ee1ce649
@ -0,0 +1,36 @@
|
||||
<?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 $input
|
||||
* @return \App\Models\User
|
||||
*/
|
||||
public function create(array $input)
|
||||
{
|
||||
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']),
|
||||
]);
|
||||
}
|
||||
}
|
@ -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|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
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 mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
*/
|
||||
public function update($user, array $input)
|
||||
{
|
||||
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 mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
*/
|
||||
protected function updateVerifiedUser($user, array $input)
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Jetstream;
|
||||
|
||||
use Laravel\Jetstream\Contracts\DeletesUsers;
|
||||
|
||||
class DeleteUser implements DeletesUsers
|
||||
{
|
||||
/**
|
||||
* Delete the given user.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @return void
|
||||
*/
|
||||
public function delete($user)
|
||||
{
|
||||
$user->deleteProfilePhoto();
|
||||
$user->tokens->each->delete();
|
||||
$user->delete();
|
||||
}
|
||||
}
|
@ -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'));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->configurePermissions();
|
||||
|
||||
Jetstream::deleteUsersUsing(DeleteUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the permissions that are available within the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configurePermissions()
|
||||
{
|
||||
Jetstream::defaultApiTokenPermissions(['read']);
|
||||
|
||||
Jetstream::permissions([
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
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' => RouteServiceProvider::HOME,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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,
|
||||
]),
|
||||
],
|
||||
|
||||
];
|
@ -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',
|
||||
|
||||
];
|
@ -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,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSessionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->text('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -1 +1,7 @@
|
||||
require('./bootstrap');
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
|
@ -0,0 +1,3 @@
|
||||
# Privacy Policy
|
||||
|
||||
Edit this file to define the privacy policy for your application.
|
@ -0,0 +1,3 @@
|
||||
# Terms of Service
|
||||
|
||||
Edit this file to define the terms of service for your application.
|
@ -0,0 +1,169 @@
|
||||
<div>
|
||||
<!-- Generate API Token -->
|
||||
<x-jet-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-jet-label for="name" value="{{ __('Token Name') }}" />
|
||||
<x-jet-input id="name" type="text" class="mt-1 block w-full" wire:model.defer="createApiTokenForm.name" autofocus />
|
||||
<x-jet-input-error for="name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Token Permissions -->
|
||||
@if (Laravel\Jetstream\Jetstream::hasPermissions())
|
||||
<div class="col-span-6">
|
||||
<x-jet-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-jet-checkbox wire:model.defer="createApiTokenForm.permissions" :value="$permission"/>
|
||||
<span class="ml-2 text-sm text-gray-600">{{ $permission }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-jet-action-message class="mr-3" on="created">
|
||||
{{ __('Created.') }}
|
||||
</x-jet-action-message>
|
||||
|
||||
<x-jet-button>
|
||||
{{ __('Create') }}
|
||||
</x-jet-button>
|
||||
</x-slot>
|
||||
</x-jet-form-section>
|
||||
|
||||
@if ($this->user->tokens->isNotEmpty())
|
||||
<x-jet-section-border />
|
||||
|
||||
<!-- Manage API Tokens -->
|
||||
<div class="mt-10 sm:mt-0">
|
||||
<x-jet-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>
|
||||
{{ $token->name }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
@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 ml-6 text-sm text-gray-400 underline" wire:click="manageApiTokenPermissions({{ $token->id }})">
|
||||
{{ __('Permissions') }}
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<button class="cursor-pointer ml-6 text-sm text-red-500" wire:click="confirmApiTokenDeletion({{ $token->id }})">
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jet-action-section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Token Value Modal -->
|
||||
<x-jet-dialog-modal wire:model="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-jet-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"
|
||||
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-jet-secondary-button wire:click="$set('displayingToken', false)" wire:loading.attr="disabled">
|
||||
{{ __('Close') }}
|
||||
</x-jet-secondary-button>
|
||||
</x-slot>
|
||||
</x-jet-dialog-modal>
|
||||
|
||||
<!-- API Token Permissions Modal -->
|
||||
<x-jet-dialog-modal wire:model="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-jet-checkbox wire:model.defer="updateApiTokenForm.permissions" :value="$permission"/>
|
||||
<span class="ml-2 text-sm text-gray-600">{{ $permission }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jet-secondary-button wire:click="$set('managingApiTokenPermissions', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jet-secondary-button>
|
||||
|
||||
<x-jet-button class="ml-3" wire:click="updateApiToken" wire:loading.attr="disabled">
|
||||
{{ __('Save') }}
|
||||
</x-jet-button>
|
||||
</x-slot>
|
||||
</x-jet-dialog-modal>
|
||||
|
||||
<!-- Delete Token Confirmation Modal -->
|
||||
<x-jet-confirmation-modal wire:model="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-jet-secondary-button wire:click="$toggle('confirmingApiTokenDeletion')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jet-secondary-button>
|
||||
|
||||
<x-jet-danger-button class="ml-3" wire:click="deleteApiToken" wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jet-danger-button>
|
||||
</x-slot>
|
||||
</x-jet-confirmation-modal>
|
||||
</div>
|
@ -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>
|
@ -0,0 +1,28 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-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-jet-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-jet-label for="password" value="{{ __('Password') }}" />
|
||||
<x-jet-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-jet-button class="ml-4">
|
||||
{{ __('Confirm') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,34 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-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>
|
||||
|
||||
@if (session('status'))
|
||||
<div class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-jet-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}">
|
||||
@csrf
|
||||
|
||||
<div class="block">
|
||||
<x-jet-label for="email" value="{{ __('Email') }}" />
|
||||
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-jet-button>
|
||||
{{ __('Email Password Reset Link') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,48 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-jet-validation-errors class="mb-4" />
|
||||
|
||||
@if (session('status'))
|
||||
<div class="mb-4 font-medium text-sm text-green-600">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-jet-label for="email" value="{{ __('Email') }}" />
|
||||
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-jet-label for="password" value="{{ __('Password') }}" />
|
||||
<x-jet-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-jet-checkbox id="remember_me" name="remember" />
|
||||
<span class="ml-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" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-jet-button class="ml-4">
|
||||
{{ __('Log in') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,60 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-jet-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<x-jet-label for="name" value="{{ __('Name') }}" />
|
||||
<x-jet-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-jet-label for="email" value="{{ __('Email') }}" />
|
||||
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-jet-label for="password" value="{{ __('Password') }}" />
|
||||
<x-jet-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-jet-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-jet-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-jet-label for="terms">
|
||||
<div class="flex items-center">
|
||||
<x-jet-checkbox name="terms" id="terms"/>
|
||||
|
||||
<div class="ml-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">'.__('Terms of Service').'</a>',
|
||||
'privacy_policy' => '<a target="_blank" href="'.route('policy.show').'" class="underline text-sm text-gray-600 hover:text-gray-900">'.__('Privacy Policy').'</a>',
|
||||
]) !!}
|
||||
</div>
|
||||
</div>
|
||||
</x-jet-label>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
<x-jet-button class="ml-4">
|
||||
{{ __('Register') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,36 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-authentication-card-logo />
|
||||
</x-slot>
|
||||
|
||||
<x-jet-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-jet-label for="email" value="{{ __('Email') }}" />
|
||||
<x-jet-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-jet-label for="password" value="{{ __('Password') }}" />
|
||||
<x-jet-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-jet-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-jet-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-jet-button>
|
||||
{{ __('Reset Password') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,57 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-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-show="recovery">
|
||||
{{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }}
|
||||
</div>
|
||||
|
||||
<x-jet-validation-errors class="mb-4" />
|
||||
|
||||
<form method="POST" action="{{ route('two-factor.login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mt-4" x-show="! recovery">
|
||||
<x-jet-label for="code" value="{{ __('Code') }}" />
|
||||
<x-jet-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-show="recovery">
|
||||
<x-jet-label for="recovery_code" value="{{ __('Recovery Code') }}" />
|
||||
<x-jet-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-show="recovery"
|
||||
x-on:click="
|
||||
recovery = false;
|
||||
$nextTick(() => { $refs.code.focus() })
|
||||
">
|
||||
{{ __('Use an authentication code') }}
|
||||
</button>
|
||||
|
||||
<x-jet-button class="ml-4">
|
||||
{{ __('Log in') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -0,0 +1,45 @@
|
||||
<x-guest-layout>
|
||||
<x-jet-authentication-card>
|
||||
<x-slot name="logo">
|
||||
<x-jet-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-jet-button type="submit">
|
||||
{{ __('Resend Verification Email') }}
|
||||
</x-jet-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="{{ route('profile.show') }}"
|
||||
class="underline text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
{{ __('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 ml-2">
|
||||
{{ __('Log Out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-jet-authentication-card>
|
||||
</x-guest-layout>
|
@ -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-jet-welcome />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
@ -0,0 +1,44 @@
|
||||
<!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="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
|
||||
|
||||
<!-- Styles -->
|
||||
@livewireStyles
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<x-jet-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>
|
@ -0,0 +1,21 @@
|
||||
<!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="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="font-sans text-gray-900 antialiased">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,215 @@
|
||||
<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-jet-application-mark class="block h-9 w-auto" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||
<x-jet-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-jet-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:items-center sm:ml-6">
|
||||
<!-- Teams Dropdown -->
|
||||
@if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
|
||||
<div class="ml-3 relative">
|
||||
<x-jet-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:bg-gray-50 hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition">
|
||||
{{ Auth::user()->currentTeam->name }}
|
||||
|
||||
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</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-jet-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
|
||||
{{ __('Team Settings') }}
|
||||
</x-jet-dropdown-link>
|
||||
|
||||
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
|
||||
<x-jet-dropdown-link href="{{ route('teams.create') }}">
|
||||
{{ __('Create New Team') }}
|
||||
</x-jet-dropdown-link>
|
||||
@endcan
|
||||
|
||||
<div class="border-t border-gray-100"></div>
|
||||
|
||||
<!-- Team Switcher -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Switch Teams') }}
|
||||
</div>
|
||||
|
||||
@foreach (Auth::user()->allTeams() as $team)
|
||||
<x-jet-switchable-team :team="$team" />
|
||||
@endforeach
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jet-dropdown>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="ml-3 relative">
|
||||
<x-jet-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="h-8 w-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 transition">
|
||||
{{ Auth::user()->name }}
|
||||
|
||||
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</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-jet-dropdown-link href="{{ route('profile.show') }}">
|
||||
{{ __('Profile') }}
|
||||
</x-jet-dropdown-link>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-jet-dropdown-link href="{{ route('api-tokens.index') }}">
|
||||
{{ __('API Tokens') }}
|
||||
</x-jet-dropdown-link>
|
||||
@endif
|
||||
|
||||
<div class="border-t border-gray-100"></div>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-jet-dropdown-link href="{{ route('logout') }}"
|
||||
@click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-jet-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-jet-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-mr-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">
|
||||
<svg class="h-6 w-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-jet-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-jet-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 mr-3">
|
||||
<img class="h-10 w-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-jet-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
|
||||
{{ __('Profile') }}
|
||||
</x-jet-responsive-nav-link>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<x-jet-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
|
||||
{{ __('API Tokens') }}
|
||||
</x-jet-responsive-nav-link>
|
||||
@endif
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
@csrf
|
||||
|
||||
<x-jet-responsive-nav-link href="{{ route('logout') }}"
|
||||
@click.prevent="$root.submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-jet-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-jet-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
|
||||
{{ __('Team Settings') }}
|
||||
</x-jet-responsive-nav-link>
|
||||
|
||||
@can('create', Laravel\Jetstream\Jetstream::newTeamModel())
|
||||
<x-jet-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
|
||||
{{ __('Create New Team') }}
|
||||
</x-jet-responsive-nav-link>
|
||||
@endcan
|
||||
|
||||
<div class="border-t border-gray-200"></div>
|
||||
|
||||
<!-- Team Switcher -->
|
||||
<div class="block px-4 py-2 text-xs text-gray-400">
|
||||
{{ __('Switch Teams') }}
|
||||
</div>
|
||||
|
||||
@foreach (Auth::user()->allTeams() as $team)
|
||||
<x-jet-switchable-team :team="$team" component="jet-responsive-nav-link" />
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
@ -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-jet-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>
|
@ -0,0 +1,52 @@
|
||||
<x-jet-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-jet-danger-button wire:click="confirmUserDeletion" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-jet-danger-button>
|
||||
</div>
|
||||
|
||||
<!-- Delete User Confirmation Modal -->
|
||||
<x-jet-dialog-modal wire:model="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-jet-input type="password" class="mt-1 block w-3/4"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
wire:model.defer="password"
|
||||
wire:keydown.enter="deleteUser" />
|
||||
|
||||
<x-jet-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jet-secondary-button wire:click="$toggle('confirmingUserDeletion')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jet-secondary-button>
|
||||
|
||||
<x-jet-danger-button class="ml-3" wire:click="deleteUser" wire:loading.attr="disabled">
|
||||
{{ __('Delete Account') }}
|
||||
</x-jet-danger-button>
|
||||
</x-slot>
|
||||
</x-jet-dialog-modal>
|
||||
</x-slot>
|
||||
</x-jet-action-section>
|
@ -0,0 +1,97 @@
|
||||
<x-jet-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 fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor" class="w-8 h-8 text-gray-500">
|
||||
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
@else
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" class="w-8 h-8 text-gray-500">
|
||||
<path d="M0 0h24v24H0z" stroke="none"></path><rect x="7" y="4" width="10" height="16" rx="1"></rect><path d="M11 5h2M12 17v.01"></path>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="ml-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-jet-button wire:click="confirmLogout" wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-jet-button>
|
||||
|
||||
<x-jet-action-message class="ml-3" on="loggedOut">
|
||||
{{ __('Done.') }}
|
||||
</x-jet-action-message>
|
||||
</div>
|
||||
|
||||
<!-- Log Out Other Devices Confirmation Modal -->
|
||||
<x-jet-dialog-modal wire:model="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-jet-input type="password" class="mt-1 block w-3/4"
|
||||
placeholder="{{ __('Password') }}"
|
||||
x-ref="password"
|
||||
wire:model.defer="password"
|
||||
wire:keydown.enter="logoutOtherBrowserSessions" />
|
||||
|
||||
<x-jet-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jet-secondary-button wire:click="$toggle('confirmingLogout')" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jet-secondary-button>
|
||||
|
||||
<x-jet-button class="ml-3"
|
||||
wire:click="logoutOtherBrowserSessions"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Log Out Other Browser Sessions') }}
|
||||
</x-jet-button>
|
||||
</x-slot>
|
||||
</x-jet-dialog-modal>
|
||||
</x-slot>
|
||||
</x-jet-action-section>
|
@ -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-jet-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-jet-section-border />
|
||||
@endif
|
||||
|
||||
@if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.two-factor-authentication-form')
|
||||
</div>
|
||||
|
||||
<x-jet-section-border />
|
||||
@endif
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.logout-other-browser-sessions-form')
|
||||
</div>
|
||||
|
||||
@if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures())
|
||||
<x-jet-section-border />
|
||||
|
||||
<div class="mt-10 sm:mt-0">
|
||||
@livewire('profile.delete-user-form')
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
@ -0,0 +1,124 @@
|
||||
<x-jet-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">
|
||||
{!! $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-jet-label for="code" value="{{ __('Code') }}" />
|
||||
|
||||
<x-jet-input id="code" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
|
||||
wire:model.defer="code"
|
||||
wire:keydown.enter="confirmTwoFactorAuthentication" />
|
||||
|
||||
<x-jet-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-jet-confirms-password wire:then="enableTwoFactorAuthentication">
|
||||
<x-jet-button type="button" wire:loading.attr="disabled">
|
||||
{{ __('Enable') }}
|
||||
</x-jet-button>
|
||||
</x-jet-confirms-password>
|
||||
@else
|
||||
@if ($showingRecoveryCodes)
|
||||
<x-jet-confirms-password wire:then="regenerateRecoveryCodes">
|
||||
<x-jet-secondary-button class="mr-3">
|
||||
{{ __('Regenerate Recovery Codes') }}
|
||||
</x-jet-secondary-button>
|
||||
</x-jet-confirms-password>
|
||||
@elseif ($showingConfirmation)
|
||||
<x-jet-confirms-password wire:then="confirmTwoFactorAuthentication">
|
||||
<x-jet-button type="button" class="mr-3" wire:loading.attr="disabled">
|
||||
{{ __('Confirm') }}
|
||||
</x-jet-button>
|
||||
</x-jet-confirms-password>
|
||||
@else
|
||||
<x-jet-confirms-password wire:then="showRecoveryCodes">
|
||||
<x-jet-secondary-button class="mr-3">
|
||||
{{ __('Show Recovery Codes') }}
|
||||
</x-jet-secondary-button>
|
||||
</x-jet-confirms-password>
|
||||
@endif
|
||||
|
||||
@if ($showingConfirmation)
|
||||
<x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-jet-secondary-button wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jet-secondary-button>
|
||||
</x-jet-confirms-password>
|
||||
@else
|
||||
<x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
|
||||
<x-jet-danger-button wire:loading.attr="disabled">
|
||||
{{ __('Disable') }}
|
||||
</x-jet-danger-button>
|
||||
</x-jet-confirms-password>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-jet-action-section>
|
@ -0,0 +1,39 @@
|
||||
<x-jet-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-jet-label for="current_password" value="{{ __('Current Password') }}" />
|
||||
<x-jet-input id="current_password" type="password" class="mt-1 block w-full" wire:model.defer="state.current_password" autocomplete="current-password" />
|
||||
<x-jet-input-error for="current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-jet-label for="password" value="{{ __('New Password') }}" />
|
||||
<x-jet-input id="password" type="password" class="mt-1 block w-full" wire:model.defer="state.password" autocomplete="new-password" />
|
||||
<x-jet-input-error for="password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-jet-label for="password_confirmation" value="{{ __('Confirm Password') }}" />
|
||||
<x-jet-input id="password_confirmation" type="password" class="mt-1 block w-full" wire:model.defer="state.password_confirmation" autocomplete="new-password" />
|
||||
<x-jet-input-error for="password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="actions">
|
||||
<x-jet-action-message class="mr-3" on="saved">
|
||||
{{ __('Saved.') }}
|
||||
</x-jet-action-message>
|
||||
|
||||
<x-jet-button>
|
||||
{{ __('Save') }}
|
||||
</x-jet-button>
|
||||
</x-slot>
|
||||
</x-jet-form-section>
|
@ -0,0 +1,95 @@
|
||||
<x-jet-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" class="hidden"
|
||||
wire:model="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-jet-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 h-20 w-20 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Profile Photo Preview -->
|
||||
<div class="mt-2" x-show="photoPreview" style="display: none;">
|
||||
<span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
|
||||
x-bind:style="'background-image: url(\'' + photoPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-jet-secondary-button class="mt-2 mr-2" type="button" x-on:click.prevent="$refs.photo.click()">
|
||||
{{ __('Select A New Photo') }}
|
||||
</x-jet-secondary-button>
|
||||
|
||||
@if ($this->user->profile_photo_path)
|
||||
<x-jet-secondary-button type="button" class="mt-2" wire:click="deleteProfilePhoto">
|
||||
{{ __('Remove Photo') }}
|
||||
</x-jet-secondary-button>
|
||||
@endif
|
||||
|
||||
<x-jet-input-error for="photo" class="mt-2" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-jet-label for="name" value="{{ __('Name') }}" />
|
||||
<x-jet-input id="name" type="text" class="mt-1 block w-full" wire:model.defer="state.name" autocomplete="name" />
|
||||
<x-jet-input-error for="name" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-jet-label for="email" value="{{ __('Email') }}" />
|
||||
<x-jet-input id="email" type="email" class="mt-1 block w-full" wire:model.defer="state.email" />
|
||||
<x-jet-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" wire:click.prevent="sendEmailVerification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if ($this->verificationLinkSent)
|
||||
<p v-show="verificationLinkSent" 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-jet-action-message class="mr-3" on="saved">
|
||||
{{ __('Saved.') }}
|
||||
</x-jet-action-message>
|
||||
|
||||
<x-jet-button wire:loading.attr="disabled" wire:target="photo">
|
||||
{{ __('Save') }}
|
||||
</x-jet-button>
|
||||
</x-slot>
|
||||
</x-jet-form-section>
|
@ -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-jet-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>
|
@ -0,0 +1,21 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./vendor/laravel/jetstream/**/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/views/**/*.blade.php',
|
||||
],
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Nunito', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
|
||||
};
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApiTokenPermissionsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_api_token_permissions_can_be_updated()
|
||||
{
|
||||
if (! Features::hasApiFeatures()) {
|
||||
return $this->markTestSkipped('API support is not enabled.');
|
||||
}
|
||||
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->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');
|
||||
|
||||
$this->assertTrue($user->fresh()->tokens->first()->can('delete'));
|
||||
$this->assertFalse($user->fresh()->tokens->first()->can('read'));
|
||||
$this->assertFalse($user->fresh()->tokens->first()->can('missing-permission'));
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered()
|
||||
{
|
||||
$response = $this->get('/login');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BrowserSessionsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_other_browser_sessions_can_be_logged_out()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(LogoutOtherBrowserSessionsForm::class)
|
||||
->set('password', 'password')
|
||||
->call('logoutOtherBrowserSessions');
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CreateApiTokenTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_api_tokens_can_be_created()
|
||||
{
|
||||
if (! Features::hasApiFeatures()) {
|
||||
return $this->markTestSkipped('API support is not enabled.');
|
||||
}
|
||||
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->create());
|
||||
|
||||
Livewire::test(ApiTokenManager::class)
|
||||
->set(['createApiTokenForm' => [
|
||||
'name' => 'Test Token',
|
||||
'permissions' => [
|
||||
'read',
|
||||
'update',
|
||||
],
|
||||
]])
|
||||
->call('createApiToken');
|
||||
|
||||
$this->assertCount(1, $user->fresh()->tokens);
|
||||
$this->assertEquals('Test Token', $user->fresh()->tokens->first()->name);
|
||||
$this->assertTrue($user->fresh()->tokens->first()->can('read'));
|
||||
$this->assertFalse($user->fresh()->tokens->first()->can('delete'));
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\DeleteUserForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeleteAccountTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_accounts_can_be_deleted()
|
||||
{
|
||||
if (! Features::hasAccountDeletionFeatures()) {
|
||||
return $this->markTestSkipped('Account deletion is not enabled.');
|
||||
}
|
||||
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$component = Livewire::test(DeleteUserForm::class)
|
||||
->set('password', 'password')
|
||||
->call('deleteUser');
|
||||
|
||||
$this->assertNull($user->fresh());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_before_account_can_be_deleted()
|
||||
{
|
||||
if (! Features::hasAccountDeletionFeatures()) {
|
||||
return $this->markTestSkipped('Account deletion is not enabled.');
|
||||
}
|
||||
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(DeleteUserForm::class)
|
||||
->set('password', 'wrong-password')
|
||||
->call('deleteUser')
|
||||
->assertHasErrors(['password']);
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeleteApiTokenTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_api_tokens_can_be_deleted()
|
||||
{
|
||||
if (! Features::hasApiFeatures()) {
|
||||
return $this->markTestSkipped('API support is not enabled.');
|
||||
}
|
||||
|
||||
$this->actingAs($user = User::factory()->withPersonalTeam()->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');
|
||||
|
||||
$this->assertCount(0, $user->fresh()->tokens);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered()
|
||||
{
|
||||
if (! Features::enabled(Features::emailVerification())) {
|
||||
return $this->markTestSkipped('Email verification not enabled.');
|
||||
}
|
||||
|
||||
$user = User::factory()->withPersonalTeam()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/email/verify');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified()
|
||||
{
|
||||
if (! Features::enabled(Features::emailVerification())) {
|
||||
return $this->markTestSkipped('Email verification not enabled.');
|
||||
}
|
||||
|
||||
Event::fake();
|
||||
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(RouteServiceProvider::HOME.'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_can_not_verified_with_invalid_hash()
|
||||
{
|
||||
if (! Features::enabled(Features::emailVerification())) {
|
||||
return $this->markTestSkipped('Email verification not enabled.');
|
||||
}
|
||||
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered()
|
||||
{
|
||||
$user = User::factory()->withPersonalTeam()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/user/confirm-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_password_can_be_confirmed()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/user/confirm-password', [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHasNoErrors();
|
||||
}
|
||||
|
||||
public function test_password_is_not_confirmed_with_invalid_password()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post('/user/confirm-password', [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors();
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered()
|
||||
{
|
||||
if (! Features::enabled(Features::resetPasswords())) {
|
||||
return $this->markTestSkipped('Password updates are not enabled.');
|
||||
}
|
||||
|
||||
$response = $this->get('/forgot-password');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested()
|
||||
{
|
||||
if (! Features::enabled(Features::resetPasswords())) {
|
||||
return $this->markTestSkipped('Password updates are not enabled.');
|
||||
}
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered()
|
||||
{
|
||||
if (! Features::enabled(Features::resetPasswords())) {
|
||||
return $this->markTestSkipped('Password updates are not enabled.');
|
||||
}
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get('/reset-password/'.$notification->token);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token()
|
||||
{
|
||||
if (! Features::enabled(Features::resetPasswords())) {
|
||||
return $this->markTestSkipped('Password updates are not enabled.');
|
||||
}
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post('/forgot-password', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post('/reset-password', [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileInformationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_current_profile_information_is_available()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$component = Livewire::test(UpdateProfileInformationForm::class);
|
||||
|
||||
$this->assertEquals($user->name, $component->state['name']);
|
||||
$this->assertEquals($user->email, $component->state['email']);
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdateProfileInformationForm::class)
|
||||
->set('state', ['name' => 'Test Name', 'email' => 'test@example.com'])
|
||||
->call('updateProfileInformation');
|
||||
|
||||
$this->assertEquals('Test Name', $user->fresh()->name);
|
||||
$this->assertEquals('test@example.com', $user->fresh()->email);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_registration_screen_can_be_rendered()
|
||||
{
|
||||
if (! Features::enabled(Features::registration())) {
|
||||
return $this->markTestSkipped('Registration support is not enabled.');
|
||||
}
|
||||
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_registration_screen_cannot_be_rendered_if_support_is_disabled()
|
||||
{
|
||||
if (Features::enabled(Features::registration())) {
|
||||
return $this->markTestSkipped('Registration support is enabled.');
|
||||
}
|
||||
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
public function test_new_users_can_register()
|
||||
{
|
||||
if (! Features::enabled(Features::registration())) {
|
||||
return $this->markTestSkipped('Registration support is not enabled.');
|
||||
}
|
||||
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TwoFactorAuthenticationSettingsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_two_factor_authentication_can_be_enabled()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$this->assertNotNull($user->two_factor_secret);
|
||||
$this->assertCount(8, $user->recoveryCodes());
|
||||
}
|
||||
|
||||
public function test_recovery_codes_can_be_regenerated()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$component = Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication')
|
||||
->call('regenerateRecoveryCodes');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$component->call('regenerateRecoveryCodes');
|
||||
|
||||
$this->assertCount(8, $user->recoveryCodes());
|
||||
$this->assertCount(8, array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes()));
|
||||
}
|
||||
|
||||
public function test_two_factor_authentication_can_be_disabled()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$this->withSession(['auth.password_confirmed_at' => time()]);
|
||||
|
||||
$component = Livewire::test(TwoFactorAuthenticationForm::class)
|
||||
->call('enableTwoFactorAuthentication');
|
||||
|
||||
$this->assertNotNull($user->fresh()->two_factor_secret);
|
||||
|
||||
$component->call('disableTwoFactorAuthentication');
|
||||
|
||||
$this->assertNull($user->fresh()->two_factor_secret);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdatePasswordTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_password_can_be_updated()
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
Livewire::test(UpdatePasswordForm::class)
|
||||
->set('state', [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
->call('updatePassword');
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->fresh()->password));
|
||||
}
|
||||
|
||||
public function test_current_password_must_be_correct()
|
||||
{
|
||||
$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']);
|
||||
|
||||
$this->assertTrue(Hash::check('password', $user->fresh()->password));
|
||||
}
|
||||
|
||||
public function test_new_passwords_must_match()
|
||||
{
|
||||
$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']);
|
||||
|
||||
$this->assertTrue(Hash::check('password', $user->fresh()->password));
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel([
|
||||
'resources/css/app.css',
|
||||
'resources/js/app.js',
|
||||
]),
|
||||
],
|
||||
});
|
Loading…
Reference in new issue