tipo: modificación del cpanel y gitignore de la carpeta vendor

This commit is contained in:
2026-07-07 11:35:09 -06:00
parent a0c91dc567
commit 676ef0d506
8510 changed files with 1132803 additions and 4 deletions
@@ -0,0 +1,38 @@
<?php
namespace Laravel\Jetstream\Actions;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Events\TeamMemberUpdated;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Rules\Role;
class UpdateTeamMemberRole
{
/**
* Update the role for the given team member.
*
* @param mixed $user
* @param mixed $team
* @param int $teamMemberId
* @param string $role
* @return void
*/
public function update($user, $team, $teamMemberId, string $role)
{
Gate::forUser($user)->authorize('updateTeamMember', $team);
Validator::make([
'role' => $role,
], [
'role' => ['required', 'string', new Role],
])->validate();
$team->users()->updateExistingPivot($teamMemberId, [
'role' => $role,
]);
TeamMemberUpdated::dispatch($team->fresh(), Jetstream::findUserByIdOrFail($teamMemberId));
}
}
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream\Actions;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
class ValidateTeamDeletion
{
/**
* Validate that the team can be deleted by the given user.
*
* @param mixed $user
* @param mixed $team
* @return void
*/
public function validate($user, $team)
{
Gate::forUser($user)->authorize('delete', $team);
if ($team->personal_team) {
throw ValidationException::withMessages([
'team' => __('You may not delete your personal team.'),
])->errorBag('deleteTeam');
}
}
}
+172
View File
@@ -0,0 +1,172 @@
<?php
namespace Laravel\Jetstream;
use Closure;
use Detection\MobileDetect;
/**
* @copyright Originally created by Jens Segers: https://github.com/jenssegers/agent
*/
class Agent extends MobileDetect
{
/**
* List of additional operating systems.
*
* @var array<string, string>
*/
protected static $additionalOperatingSystems = [
'Windows' => 'Windows',
'Windows NT' => 'Windows NT',
'OS X' => 'Mac OS X',
'Debian' => 'Debian',
'Ubuntu' => 'Ubuntu',
'Macintosh' => 'PPC',
'OpenBSD' => 'OpenBSD',
'Linux' => 'Linux',
'ChromeOS' => 'CrOS',
];
/**
* List of additional browsers.
*
* @var array<string, string>
*/
protected static $additionalBrowsers = [
'Opera Mini' => 'Opera Mini',
'Opera' => 'Opera|OPR',
'Edge' => 'Edge|Edg',
'Coc Coc' => 'coc_coc_browser',
'UCBrowser' => 'UCBrowser',
'Vivaldi' => 'Vivaldi',
'Chrome' => 'Chrome',
'Firefox' => 'Firefox',
'Safari' => 'Safari',
'IE' => 'MSIE|IEMobile|MSIEMobile|Trident/[.0-9]+',
'Netscape' => 'Netscape',
'Mozilla' => 'Mozilla',
'WeChat' => 'MicroMessenger',
];
/**
* Key value store for resolved strings.
*
* @var array<string, mixed>
*/
protected $store = [];
/**
* Get the platform name from the User Agent.
*
* @return string|null
*/
public function platform()
{
return $this->retrieveUsingCacheOrResolve('jetstream.platform', function () {
return $this->findDetectionRulesAgainstUserAgent(
$this->mergeRules(MobileDetect::getOperatingSystems(), static::$additionalOperatingSystems)
);
});
}
/**
* Get the browser name from the User Agent.
*
* @return string|null
*/
public function browser()
{
return $this->retrieveUsingCacheOrResolve('jetstream.browser', function () {
return $this->findDetectionRulesAgainstUserAgent(
$this->mergeRules(static::$additionalBrowsers, MobileDetect::getBrowsers())
);
});
}
/**
* Determine if the device is a desktop computer.
*
* @return bool
*/
public function isDesktop()
{
return $this->retrieveUsingCacheOrResolve('jetstream.desktop', function () {
// Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront'
if (
$this->getUserAgent() === static::$cloudFrontUA
&& $this->getHttpHeader('HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER') === 'true'
) {
return true;
}
return ! $this->isMobile() && ! $this->isTablet();
});
}
/**
* Match a detection rule and return the matched key.
*
* @return string|null
*/
protected function findDetectionRulesAgainstUserAgent(array $rules)
{
$userAgent = $this->getUserAgent();
foreach ($rules as $key => $regex) {
if (empty($regex)) {
continue;
}
if ($this->match($regex, $userAgent)) {
return $key ?: reset($this->matchesArray);
}
}
return null;
}
/**
* Retrieve from the given key from the cache or resolve the value.
*
* @param string $key
* @param \Closure():mixed $callback
* @return mixed
*/
protected function retrieveUsingCacheOrResolve(string $key, Closure $callback)
{
$cacheKey = $this->createCacheKey($key);
if (! is_null($cacheItem = $this->store[$cacheKey] ?? null)) {
return $cacheItem;
}
return tap(call_user_func($callback), function ($result) use ($cacheKey) {
$this->store[$cacheKey] = $result;
});
}
/**
* Merge multiple rules into one array.
*
* @param array $all
* @return array<string, string>
*/
protected function mergeRules(...$all)
{
$merged = [];
foreach ($all as $rules) {
foreach ($rules as $key => $value) {
if (empty($merged[$key])) {
$merged[$key] = $value;
} elseif (is_array($merged[$key])) {
$merged[$key][] = $value;
} else {
$merged[$key] .= '|'.$value;
}
}
}
return $merged;
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmPassword;
trait ConfirmsPasswords
{
/**
* Indicates if the user's password is being confirmed.
*
* @var bool
*/
public $confirmingPassword = false;
/**
* The ID of the operation being confirmed.
*
* @var string|null
*/
public $confirmableId = null;
/**
* The user's password.
*
* @var string
*/
public $confirmablePassword = '';
/**
* Start confirming the user's password.
*
* @param string $confirmableId
* @return void
*/
public function startConfirmingPassword(string $confirmableId)
{
$this->resetErrorBag();
if ($this->passwordIsConfirmed()) {
return $this->dispatch('password-confirmed',
id: $confirmableId,
);
}
$this->confirmingPassword = true;
$this->confirmableId = $confirmableId;
$this->confirmablePassword = '';
$this->dispatch('confirming-password');
}
/**
* Stop confirming the user's password.
*
* @return void
*/
public function stopConfirmingPassword()
{
$this->confirmingPassword = false;
$this->confirmableId = null;
$this->confirmablePassword = '';
}
/**
* Confirm the user's password.
*
* @return void
*/
public function confirmPassword()
{
if (! app(ConfirmPassword::class)(app(StatefulGuard::class), Auth::user(), $this->confirmablePassword)) {
throw ValidationException::withMessages([
'confirmable_password' => [__('This password does not match our records.')],
]);
}
session(['auth.password_confirmed_at' => time()]);
$this->dispatch('password-confirmed',
id: $this->confirmableId,
);
$this->stopConfirmingPassword();
}
/**
* Ensure that the user's password has been recently confirmed.
*
* @param int|null $maximumSecondsSinceConfirmation
* @return void
*/
protected function ensurePasswordIsConfirmed($maximumSecondsSinceConfirmation = null)
{
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);
$this->passwordIsConfirmed($maximumSecondsSinceConfirmation) ? null : abort(403);
}
/**
* Determine if the user's password has been recently confirmed.
*
* @param int|null $maximumSecondsSinceConfirmation
* @return bool
*/
protected function passwordIsConfirmed($maximumSecondsSinceConfirmation = null)
{
$maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900);
return (time() - session('auth.password_confirmed_at', 0)) < $maximumSecondsSinceConfirmation;
}
}
+901
View File
@@ -0,0 +1,901 @@
<?php
namespace Laravel\Jetstream\Console;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
#[AsCommand(name: 'jetstream:install')]
class InstallCommand extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'jetstream:install {stack : The development stack that should be installed (inertia,livewire)}
{--dark : Indicate that dark mode support should be installed}
{--teams : Indicates if team support should be installed}
{--api : Indicates if API support should be installed}
{--verification : Indicates if email verification support should be installed}
{--pest : Indicates if Pest should be installed}
{--ssr : Indicates if Inertia SSR support should be installed}
{--composer=global : Absolute path to the Composer binary which should be used to install packages}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install the Jetstream components and resources';
/**
* Execute the console command.
*
* @return int|null
*/
public function handle()
{
if (! in_array($this->argument('stack'), ['inertia', 'livewire'])) {
$this->components->error('Invalid stack. Supported stacks are [inertia] and [livewire].');
return 1;
}
// Publish...
$this->callSilent('vendor:publish', ['--tag' => 'jetstream-config', '--force' => true]);
$this->callSilent('vendor:publish', ['--tag' => 'jetstream-migrations', '--force' => true]);
$this->callSilent('vendor:publish', ['--tag' => 'fortify-config', '--force' => true]);
$this->callSilent('vendor:publish', ['--tag' => 'fortify-support', '--force' => true]);
$this->callSilent('vendor:publish', ['--tag' => 'fortify-migrations', '--force' => true]);
// Storage...
$this->callSilent('storage:link');
$this->replaceInFile('/home', '/dashboard', config_path('fortify.php'));
if (file_exists(resource_path('views/welcome.blade.php'))) {
$this->replaceInFile('/home', '/dashboard', resource_path('views/welcome.blade.php'));
$this->replaceInFile('Home', 'Dashboard', resource_path('views/welcome.blade.php'));
}
// Fortify Provider...
ServiceProvider::addProviderToBootstrapFile('App\Providers\FortifyServiceProvider');
// Configure Session...
$this->configureSession();
// Configure API...
if ($this->option('api')) {
$this->replaceInFile('// Features::api(),', 'Features::api(),', config_path('jetstream.php'));
}
// Configure Email Verification...
if ($this->option('verification')) {
$this->replaceInFile('// Features::emailVerification(),', 'Features::emailVerification(),', config_path('fortify.php'));
}
// Install Stack...
if ($this->argument('stack') === 'livewire') {
if (! $this->installLivewireStack()) {
return 1;
}
} elseif ($this->argument('stack') === 'inertia') {
if (! $this->installInertiaStack()) {
return 1;
}
}
// Emails...
(new Filesystem)->ensureDirectoryExists(resource_path('views/emails'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/resources/views/emails', resource_path('views/emails'));
// Tests...
$stubs = $this->getTestStubsPath();
if ($this->option('pest') || $this->isUsingPest()) {
if ($this->hasComposerPackage('phpunit/phpunit')) {
$this->removeComposerDevPackages(['phpunit/phpunit']);
}
if (! $this->requireComposerDevPackages(['pestphp/pest', 'pestphp/pest-plugin-laravel'])) {
return 1;
}
copy($stubs.'/Pest.php', base_path('tests/Pest.php'));
copy($stubs.'/ExampleTest.php', base_path('tests/Feature/ExampleTest.php'));
copy($stubs.'/ExampleUnitTest.php', base_path('tests/Unit/ExampleTest.php'));
}
copy($stubs.'/AuthenticationTest.php', base_path('tests/Feature/AuthenticationTest.php'));
copy($stubs.'/EmailVerificationTest.php', base_path('tests/Feature/EmailVerificationTest.php'));
copy($stubs.'/PasswordConfirmationTest.php', base_path('tests/Feature/PasswordConfirmationTest.php'));
copy($stubs.'/PasswordResetTest.php', base_path('tests/Feature/PasswordResetTest.php'));
copy($stubs.'/RegistrationTest.php', base_path('tests/Feature/RegistrationTest.php'));
}
/**
* Configure the session driver for Jetstream.
*
* @return void
*/
protected function configureSession()
{
$this->replaceInFile('SESSION_DRIVER=cookie', 'SESSION_DRIVER=database', base_path('.env'));
$this->replaceInFile('SESSION_DRIVER=cookie', 'SESSION_DRIVER=database', base_path('.env.example'));
}
/**
* Install the Livewire stack into the application.
*
* @return bool
*/
protected function installLivewireStack()
{
// Install Livewire...
if (! $this->requireComposerPackages('livewire/livewire:^3.6.4')) {
return false;
}
$this->call('install:api', [
'--without-migration-prompt' => true,
]);
// Update Configuration...
$this->replaceInFile('inertia', 'livewire', config_path('jetstream.php'));
// NPM Packages...
$this->updateNodePackages(function ($packages) {
return [
'@tailwindcss/forms' => '^0.5.7',
'@tailwindcss/typography' => '^0.5.10',
'autoprefixer' => '^10.4.16',
'postcss' => '^8.4.32',
'tailwindcss' => '^3.4.0',
] + $packages;
});
// Tailwind Configuration...
copy(__DIR__.'/../../stubs/livewire/tailwind.config.js', base_path('tailwind.config.js'));
copy(__DIR__.'/../../stubs/livewire/postcss.config.js', base_path('postcss.config.js'));
copy(__DIR__.'/../../stubs/livewire/vite.config.js', base_path('vite.config.js'));
// Directories...
(new Filesystem)->ensureDirectoryExists(app_path('Actions/Fortify'));
(new Filesystem)->ensureDirectoryExists(app_path('Actions/Jetstream'));
(new Filesystem)->ensureDirectoryExists(app_path('View/Components'));
(new Filesystem)->ensureDirectoryExists(resource_path('css'));
(new Filesystem)->ensureDirectoryExists(resource_path('markdown'));
(new Filesystem)->ensureDirectoryExists(resource_path('views/api'));
(new Filesystem)->ensureDirectoryExists(resource_path('views/auth'));
(new Filesystem)->ensureDirectoryExists(resource_path('views/components'));
(new Filesystem)->ensureDirectoryExists(resource_path('views/layouts'));
(new Filesystem)->ensureDirectoryExists(resource_path('views/profile'));
(new Filesystem)->deleteDirectory(resource_path('sass'));
// Terms Of Service / Privacy Policy...
copy(__DIR__.'/../../stubs/resources/markdown/terms.md', resource_path('markdown/terms.md'));
copy(__DIR__.'/../../stubs/resources/markdown/policy.md', resource_path('markdown/policy.md'));
// Service Providers...
copy(__DIR__.'/../../stubs/app/Providers/JetstreamServiceProvider.php', $provider = app_path('Providers/JetstreamServiceProvider.php'));
$this->replaceInFile([
PHP_EOL.'use Illuminate\Support\Facades\Vite;',
PHP_EOL.PHP_EOL.' Vite::prefetch(concurrency: 3);',
], '', $provider);
ServiceProvider::addProviderToBootstrapFile('App\Providers\JetstreamServiceProvider');
// Models...
copy(__DIR__.'/../../stubs/app/Models/User.php', app_path('Models/User.php'));
// Factories...
copy(__DIR__.'/../../database/factories/UserFactory.php', base_path('database/factories/UserFactory.php'));
// Actions...
copy(__DIR__.'/../../stubs/app/Actions/Fortify/CreateNewUser.php', app_path('Actions/Fortify/CreateNewUser.php'));
copy(__DIR__.'/../../stubs/app/Actions/Fortify/UpdateUserProfileInformation.php', app_path('Actions/Fortify/UpdateUserProfileInformation.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/DeleteUser.php', app_path('Actions/Jetstream/DeleteUser.php'));
// Components...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/components', resource_path('views/components'));
// View Components...
copy(__DIR__.'/../../stubs/livewire/app/View/Components/AppLayout.php', app_path('View/Components/AppLayout.php'));
copy(__DIR__.'/../../stubs/livewire/app/View/Components/GuestLayout.php', app_path('View/Components/GuestLayout.php'));
// Layouts...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/layouts', resource_path('views/layouts'));
// Single Blade Views...
copy(__DIR__.'/../../stubs/livewire/resources/views/dashboard.blade.php', resource_path('views/dashboard.blade.php'));
copy(__DIR__.'/../../stubs/livewire/resources/views/navigation-menu.blade.php', resource_path('views/navigation-menu.blade.php'));
copy(__DIR__.'/../../stubs/livewire/resources/views/terms.blade.php', resource_path('views/terms.blade.php'));
copy(__DIR__.'/../../stubs/livewire/resources/views/policy.blade.php', resource_path('views/policy.blade.php'));
// Other Views...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/api', resource_path('views/api'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/profile', resource_path('views/profile'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/auth', resource_path('views/auth'));
if (! Str::contains(file_get_contents(base_path('routes/web.php')), "'/dashboard'")) {
(new Filesystem)->append(base_path('routes/web.php'), $this->livewireRouteDefinition());
}
// Assets...
copy(__DIR__.'/../../stubs/resources/css/app.css', resource_path('css/app.css'));
// Tests...
$stubs = $this->getTestStubsPath();
copy($stubs.'/livewire/ApiTokenPermissionsTest.php', base_path('tests/Feature/ApiTokenPermissionsTest.php'));
copy($stubs.'/livewire/BrowserSessionsTest.php', base_path('tests/Feature/BrowserSessionsTest.php'));
copy($stubs.'/livewire/CreateApiTokenTest.php', base_path('tests/Feature/CreateApiTokenTest.php'));
copy($stubs.'/livewire/DeleteAccountTest.php', base_path('tests/Feature/DeleteAccountTest.php'));
copy($stubs.'/livewire/DeleteApiTokenTest.php', base_path('tests/Feature/DeleteApiTokenTest.php'));
copy($stubs.'/livewire/ProfileInformationTest.php', base_path('tests/Feature/ProfileInformationTest.php'));
copy($stubs.'/livewire/TwoFactorAuthenticationSettingsTest.php', base_path('tests/Feature/TwoFactorAuthenticationSettingsTest.php'));
copy($stubs.'/livewire/UpdatePasswordTest.php', base_path('tests/Feature/UpdatePasswordTest.php'));
// Teams...
if ($this->option('teams')) {
$this->installLivewireTeamStack();
}
if (! $this->option('dark')) {
$this->removeDarkClasses((new Finder)
->in(resource_path('views'))
->name('*.blade.php')
->filter(fn ($file) => $file->getPathname() !== resource_path('views/welcome.blade.php'))
);
}
if (file_exists(base_path('pnpm-lock.yaml'))) {
$this->runCommands(['pnpm install', 'pnpm run build']);
} elseif (file_exists(base_path('yarn.lock'))) {
$this->runCommands(['yarn install', 'yarn run build']);
} elseif (file_exists(base_path('bun.lockb'))) {
$this->runCommands(['bun install', 'bun run build']);
} else {
$this->runCommands(['npm install', 'npm run build']);
}
$this->line('');
$this->runDatabaseMigrations();
$this->components->info('Livewire scaffolding installed successfully.');
return true;
}
/**
* Install the Livewire team stack into the application.
*
* @return void
*/
protected function installLivewireTeamStack()
{
// Directories...
(new Filesystem)->ensureDirectoryExists(resource_path('views/teams'));
// Other Views...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/teams', resource_path('views/teams'));
// Tests...
$stubs = $this->getTestStubsPath();
copy($stubs.'/livewire/CreateTeamTest.php', base_path('tests/Feature/CreateTeamTest.php'));
copy($stubs.'/livewire/DeleteTeamTest.php', base_path('tests/Feature/DeleteTeamTest.php'));
copy($stubs.'/livewire/InviteTeamMemberTest.php', base_path('tests/Feature/InviteTeamMemberTest.php'));
copy($stubs.'/livewire/LeaveTeamTest.php', base_path('tests/Feature/LeaveTeamTest.php'));
copy($stubs.'/livewire/RemoveTeamMemberTest.php', base_path('tests/Feature/RemoveTeamMemberTest.php'));
copy($stubs.'/livewire/UpdateTeamMemberRoleTest.php', base_path('tests/Feature/UpdateTeamMemberRoleTest.php'));
copy($stubs.'/livewire/UpdateTeamNameTest.php', base_path('tests/Feature/UpdateTeamNameTest.php'));
$this->ensureApplicationIsTeamCompatible();
}
/**
* Get the route definition(s) that should be installed for Livewire.
*
* @return string
*/
protected function livewireRouteDefinition()
{
return <<<'EOF'
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
EOF;
}
/**
* Install the Inertia stack into the application.
*
* @return bool
*/
protected function installInertiaStack()
{
// Install Inertia...
if (! $this->requireComposerPackages('inertiajs/inertia-laravel:^2.0', 'tightenco/ziggy:^2.0')) {
return false;
}
$this->call('install:api', [
'--without-migration-prompt' => true,
]);
// Install NPM packages...
$this->updateNodePackages(function ($packages) {
return [
'@inertiajs/vue3' => '^2.0',
'@tailwindcss/forms' => '^0.5.7',
'@tailwindcss/typography' => '^0.5.10',
'@vitejs/plugin-vue' => '^5.0.0',
'autoprefixer' => '^10.4.16',
'postcss' => '^8.4.32',
'tailwindcss' => '^3.4.0',
'vue' => '^3.3.13',
] + $packages;
});
// Tailwind Configuration...
copy(__DIR__.'/../../stubs/inertia/tailwind.config.js', base_path('tailwind.config.js'));
copy(__DIR__.'/../../stubs/inertia/postcss.config.js', base_path('postcss.config.js'));
copy(__DIR__.'/../../stubs/inertia/vite.config.js', base_path('vite.config.js'));
// jsconfig.json...
copy(__DIR__.'/../../stubs/inertia/jsconfig.json', base_path('jsconfig.json'));
// Directories...
(new Filesystem)->ensureDirectoryExists(app_path('Actions/Fortify'));
(new Filesystem)->ensureDirectoryExists(app_path('Actions/Jetstream'));
(new Filesystem)->ensureDirectoryExists(resource_path('css'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Components'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Layouts'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Pages'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Pages/API'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Pages/Auth'));
(new Filesystem)->ensureDirectoryExists(resource_path('js/Pages/Profile'));
(new Filesystem)->ensureDirectoryExists(resource_path('views'));
(new Filesystem)->ensureDirectoryExists(resource_path('markdown'));
(new Filesystem)->deleteDirectory(resource_path('sass'));
// Terms Of Service / Privacy Policy...
copy(__DIR__.'/../../stubs/resources/markdown/terms.md', resource_path('markdown/terms.md'));
copy(__DIR__.'/../../stubs/resources/markdown/policy.md', resource_path('markdown/policy.md'));
// Service Providers...
copy(__DIR__.'/../../stubs/app/Providers/JetstreamServiceProvider.php', app_path('Providers/JetstreamServiceProvider.php'));
ServiceProvider::addProviderToBootstrapFile('App\Providers\JetstreamServiceProvider');
// Middleware...
(new Filesystem)->ensureDirectoryExists(app_path('Http/Middleware'));
(new Process([$this->phpBinary(), 'artisan', 'inertia:middleware', 'HandleInertiaRequests', '--force'], base_path()))
->setTimeout(null)
->run(function ($type, $output) {
$this->output->write($output);
});
$this->installMiddleware([
'\App\Http\Middleware\HandleInertiaRequests::class',
'\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class',
]);
// Models...
copy(__DIR__.'/../../stubs/app/Models/User.php', app_path('Models/User.php'));
// Factories...
copy(__DIR__.'/../../database/factories/UserFactory.php', base_path('database/factories/UserFactory.php'));
// Actions...
copy(__DIR__.'/../../stubs/app/Actions/Fortify/CreateNewUser.php', app_path('Actions/Fortify/CreateNewUser.php'));
copy(__DIR__.'/../../stubs/app/Actions/Fortify/UpdateUserProfileInformation.php', app_path('Actions/Fortify/UpdateUserProfileInformation.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/DeleteUser.php', app_path('Actions/Jetstream/DeleteUser.php'));
// Blade Views...
copy(__DIR__.'/../../stubs/inertia/resources/views/app.blade.php', resource_path('views/app.blade.php'));
if (file_exists(resource_path('views/welcome.blade.php'))) {
unlink(resource_path('views/welcome.blade.php'));
}
// Inertia Pages...
copy(__DIR__.'/../../stubs/inertia/resources/js/Pages/Dashboard.vue', resource_path('js/Pages/Dashboard.vue'));
copy(__DIR__.'/../../stubs/inertia/resources/js/Pages/PrivacyPolicy.vue', resource_path('js/Pages/PrivacyPolicy.vue'));
copy(__DIR__.'/../../stubs/inertia/resources/js/Pages/TermsOfService.vue', resource_path('js/Pages/TermsOfService.vue'));
copy(__DIR__.'/../../stubs/inertia/resources/js/Pages/Welcome.vue', resource_path('js/Pages/Welcome.vue'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Components', resource_path('js/Components'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Layouts', resource_path('js/Layouts'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/API', resource_path('js/Pages/API'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/Auth', resource_path('js/Pages/Auth'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/Profile', resource_path('js/Pages/Profile'));
copy(__DIR__.'/../../stubs/inertia/routes/web.php', base_path('routes/web.php'));
// Assets...
copy(__DIR__.'/../../stubs/resources/css/app.css', resource_path('css/app.css'));
copy(__DIR__.'/../../stubs/inertia/resources/js/app.js', resource_path('js/app.js'));
// Tests...
$stubs = $this->getTestStubsPath();
copy($stubs.'/inertia/ApiTokenPermissionsTest.php', base_path('tests/Feature/ApiTokenPermissionsTest.php'));
copy($stubs.'/inertia/BrowserSessionsTest.php', base_path('tests/Feature/BrowserSessionsTest.php'));
copy($stubs.'/inertia/CreateApiTokenTest.php', base_path('tests/Feature/CreateApiTokenTest.php'));
copy($stubs.'/inertia/DeleteAccountTest.php', base_path('tests/Feature/DeleteAccountTest.php'));
copy($stubs.'/inertia/DeleteApiTokenTest.php', base_path('tests/Feature/DeleteApiTokenTest.php'));
copy($stubs.'/inertia/ProfileInformationTest.php', base_path('tests/Feature/ProfileInformationTest.php'));
copy($stubs.'/inertia/TwoFactorAuthenticationSettingsTest.php', base_path('tests/Feature/TwoFactorAuthenticationSettingsTest.php'));
copy($stubs.'/inertia/UpdatePasswordTest.php', base_path('tests/Feature/UpdatePasswordTest.php'));
// Teams...
if ($this->option('teams')) {
$this->installInertiaTeamStack();
}
if ($this->option('ssr')) {
$this->installInertiaSsrStack();
}
if (! $this->option('dark')) {
$this->removeDarkClasses((new Finder)
->in(resource_path('js'))
->name('*.vue')
->notPath('Pages/Welcome.vue')
);
}
if (file_exists(base_path('pnpm-lock.yaml'))) {
$this->runCommands(['pnpm install', 'pnpm run build']);
} elseif (file_exists(base_path('yarn.lock'))) {
$this->runCommands(['yarn install', 'yarn run build']);
} elseif (file_exists(base_path('bun.lockb'))) {
$this->runCommands(['bun install', 'bun run build']);
} else {
$this->runCommands(['npm install', 'npm run build']);
}
$this->line('');
$this->runDatabaseMigrations();
$this->components->info('Inertia scaffolding installed successfully.');
return true;
}
/**
* Install the Inertia team stack into the application.
*
* @return void
*/
protected function installInertiaTeamStack()
{
// Directories...
(new Filesystem)->ensureDirectoryExists(resource_path('js/Pages/Profile'));
// Pages...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/Teams', resource_path('js/Pages/Teams'));
// Tests...
$stubs = $this->getTestStubsPath();
copy($stubs.'/inertia/CreateTeamTest.php', base_path('tests/Feature/CreateTeamTest.php'));
copy($stubs.'/inertia/DeleteTeamTest.php', base_path('tests/Feature/DeleteTeamTest.php'));
copy($stubs.'/inertia/InviteTeamMemberTest.php', base_path('tests/Feature/InviteTeamMemberTest.php'));
copy($stubs.'/inertia/LeaveTeamTest.php', base_path('tests/Feature/LeaveTeamTest.php'));
copy($stubs.'/inertia/RemoveTeamMemberTest.php', base_path('tests/Feature/RemoveTeamMemberTest.php'));
copy($stubs.'/inertia/UpdateTeamMemberRoleTest.php', base_path('tests/Feature/UpdateTeamMemberRoleTest.php'));
copy($stubs.'/inertia/UpdateTeamNameTest.php', base_path('tests/Feature/UpdateTeamNameTest.php'));
$this->ensureApplicationIsTeamCompatible();
}
/**
* Ensure the installed user model is ready for team usage.
*
* @return void
*/
protected function ensureApplicationIsTeamCompatible()
{
// Publish Team Migrations...
$this->callSilent('vendor:publish', ['--tag' => 'jetstream-team-migrations', '--force' => true]);
// Configuration...
$this->replaceInFile('// Features::teams([\'invitations\' => true])', 'Features::teams([\'invitations\' => true])', config_path('jetstream.php'));
// Directories...
(new Filesystem)->ensureDirectoryExists(app_path('Actions/Jetstream'));
(new Filesystem)->ensureDirectoryExists(app_path('Events'));
(new Filesystem)->ensureDirectoryExists(app_path('Policies'));
// Service Providers...
copy(__DIR__.'/../../stubs/app/Providers/JetstreamWithTeamsServiceProvider.php', app_path('Providers/JetstreamServiceProvider.php'));
// Models...
copy(__DIR__.'/../../stubs/app/Models/Membership.php', app_path('Models/Membership.php'));
copy(__DIR__.'/../../stubs/app/Models/Team.php', app_path('Models/Team.php'));
copy(__DIR__.'/../../stubs/app/Models/TeamInvitation.php', app_path('Models/TeamInvitation.php'));
copy(__DIR__.'/../../stubs/app/Models/UserWithTeams.php', app_path('Models/User.php'));
// Actions...
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/AddTeamMember.php', app_path('Actions/Jetstream/AddTeamMember.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/CreateTeam.php', app_path('Actions/Jetstream/CreateTeam.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/DeleteTeam.php', app_path('Actions/Jetstream/DeleteTeam.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/DeleteUserWithTeams.php', app_path('Actions/Jetstream/DeleteUser.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/InviteTeamMember.php', app_path('Actions/Jetstream/InviteTeamMember.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/RemoveTeamMember.php', app_path('Actions/Jetstream/RemoveTeamMember.php'));
copy(__DIR__.'/../../stubs/app/Actions/Jetstream/UpdateTeamName.php', app_path('Actions/Jetstream/UpdateTeamName.php'));
copy(__DIR__.'/../../stubs/app/Actions/Fortify/CreateNewUserWithTeams.php', app_path('Actions/Fortify/CreateNewUser.php'));
// Policies...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/app/Policies', app_path('Policies'));
// Factories...
copy(__DIR__.'/../../database/factories/UserFactory.php', base_path('database/factories/UserFactory.php'));
copy(__DIR__.'/../../database/factories/TeamFactory.php', base_path('database/factories/TeamFactory.php'));
// Seeders...
copy(__DIR__.'/../../database/seeders/DatabaseSeeder.php', base_path('database/seeders/DatabaseSeeder.php'));
}
/**
* Install the Inertia SSR stack into the application.
*
* @return void
*/
protected function installInertiaSsrStack()
{
$this->updateNodePackages(function ($packages) {
return [
'@vue/server-renderer' => '^3.3.13',
] + $packages;
});
copy(__DIR__.'/../../stubs/inertia/resources/js/ssr.js', resource_path('js/ssr.js'));
$this->replaceInFile("input: 'resources/js/app.js',", "input: 'resources/js/app.js',".PHP_EOL." ssr: 'resources/js/ssr.js',", base_path('vite.config.js'));
(new Filesystem)->ensureDirectoryExists(app_path('Http/Middleware'));
copy(__DIR__.'/../../stubs/inertia/app/Http/Middleware/HandleInertiaRequests.php', app_path('Http/Middleware/HandleInertiaRequests.php'));
$this->replaceInFile('vite build', 'vite build && vite build --ssr', base_path('package.json'));
$this->replaceInFile('/node_modules', '/bootstrap/ssr'.PHP_EOL.'/node_modules', base_path('.gitignore'));
}
/**
* Install the given middleware names into the application.
*
* @param array|string $name
* @param string $group
* @param string $modifier
* @return void
*/
protected function installMiddleware($names, $group = 'web', $modifier = 'append')
{
$bootstrapApp = file_get_contents(base_path('bootstrap/app.php'));
$names = collect(Arr::wrap($names))
->filter(fn ($name) => ! Str::contains($bootstrapApp, $name))
->whenNotEmpty(function ($names) use ($bootstrapApp, $group, $modifier) {
$names = $names->map(fn ($name) => "$name")->implode(','.PHP_EOL.' ');
$stubs = [
'->withMiddleware(function (Middleware $middleware) {',
'->withMiddleware(function (Middleware $middleware): void {',
];
$bootstrapApp = str_replace(
$stubs,
collect($stubs)->transform(fn ($stub) => $stub
.PHP_EOL." \$middleware->$group($modifier: ["
.PHP_EOL." $names,"
.PHP_EOL.' ]);'
.PHP_EOL
)->all(),
$bootstrapApp,
);
file_put_contents(base_path('bootstrap/app.php'), $bootstrapApp);
});
}
/**
* Returns the path to the correct test stubs.
*
* @return string
*/
protected function getTestStubsPath()
{
return $this->option('pest') || $this->isUsingPest()
? __DIR__.'/../../stubs/pest-tests'
: __DIR__.'/../../stubs/tests';
}
/**
* Determine if the given Composer package is installed.
*
* @param string $package
* @return bool
*/
protected function hasComposerPackage($package)
{
$packages = json_decode(file_get_contents(base_path('composer.json')), true);
return array_key_exists($package, $packages['require'] ?? [])
|| array_key_exists($package, $packages['require-dev'] ?? []);
}
/**
* Installs the given Composer Packages into the application.
*
* @param mixed $packages
* @return bool
*/
protected function requireComposerPackages($packages)
{
$composer = $this->option('composer');
if ($composer !== 'global') {
$command = [$this->phpBinary(), $composer, 'require'];
}
$command = array_merge(
$command ?? ['composer', 'require'],
is_array($packages) ? $packages : func_get_args()
);
return ! (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1']))
->setTimeout(null)
->run(function ($type, $output) {
$this->output->write($output);
});
}
/**
* Removes the given Composer Packages as "dev" dependencies.
*
* @param mixed $packages
* @return bool
*/
protected function removeComposerDevPackages($packages)
{
$composer = $this->option('composer');
if ($composer !== 'global') {
$command = [$this->phpBinary(), $composer, 'remove', '--dev'];
}
$command = array_merge(
$command ?? ['composer', 'remove', '--dev'],
is_array($packages) ? $packages : func_get_args()
);
return (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1']))
->setTimeout(null)
->run(function ($type, $output) {
$this->output->write($output);
}) === 0;
}
/**
* Install the given Composer Packages as "dev" dependencies.
*
* @param mixed $packages
* @return bool
*/
protected function requireComposerDevPackages($packages)
{
$composer = $this->option('composer');
if ($composer !== 'global') {
$command = [$this->phpBinary(), $composer, 'require', '--dev'];
}
$command = array_merge(
$command ?? ['composer', 'require', '--dev'],
is_array($packages) ? $packages : func_get_args()
);
return (new Process($command, base_path(), ['COMPOSER_MEMORY_LIMIT' => '-1']))
->setTimeout(null)
->run(function ($type, $output) {
$this->output->write($output);
}) === 0;
}
/**
* Update the "package.json" file.
*
* @param callable $callback
* @param bool $dev
* @return void
*/
protected static function updateNodePackages(callable $callback, $dev = true)
{
if (! file_exists(base_path('package.json'))) {
return;
}
$configurationKey = $dev ? 'devDependencies' : 'dependencies';
$packages = json_decode(file_get_contents(base_path('package.json')), true);
$packages[$configurationKey] = $callback(
array_key_exists($configurationKey, $packages) ? $packages[$configurationKey] : [],
$configurationKey
);
ksort($packages[$configurationKey]);
file_put_contents(
base_path('package.json'),
json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL
);
}
/**
* Run the database migrations.
*
* @return void
*/
protected function runDatabaseMigrations()
{
if (confirm('New database migrations were added. Would you like to re-run your migrations?', true)) {
(new Process([$this->phpBinary(), 'artisan', 'migrate:fresh', '--force'], base_path()))
->setTimeout(null)
->run(function ($type, $output) {
$this->output->write($output);
});
}
}
/**
* Replace a given string within a given file.
*
* @param string $replace
* @param string|array $search
* @param string $path
* @return void
*/
protected function replaceInFile($search, $replace, $path)
{
file_put_contents($path, str_replace($search, $replace, file_get_contents($path)));
}
/**
* Remove Tailwind dark classes from the given files.
*
* @param \Symfony\Component\Finder\Finder $finder
* @return void
*/
protected function removeDarkClasses(Finder $finder)
{
foreach ($finder as $file) {
file_put_contents($file->getPathname(), preg_replace('/\sdark:[^\s"\']+/', '', $file->getContents()));
}
}
/**
* Get the path to the appropriate PHP binary.
*
* @return string
*/
protected function phpBinary()
{
if (function_exists('Illuminate\Support\php_binary')) {
return \Illuminate\Support\php_binary();
}
return (new PhpExecutableFinder())->find(false) ?: 'php';
}
/**
* Run the given commands.
*
* @param array $commands
* @return void
*/
protected function runCommands($commands)
{
$process = Process::fromShellCommandline(implode(' && ', $commands), null, null, null, null);
if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->output->writeln(' <bg=yellow;fg=black> WARN </> '.$e->getMessage().PHP_EOL);
}
}
$process->run(function ($type, $line) {
$this->output->write(' '.$line);
});
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'stack' => fn () => select(
label: 'Which Jetstream stack would you like to install?',
options: [
'inertia' => 'Vue with Inertia',
'livewire' => 'Livewire',
]
),
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
collect(multiselect(
label: 'Would you like any optional features?',
options: collect([
'teams' => 'Team support',
'api' => 'API support',
'verification' => 'Email verification',
'dark' => 'Dark mode',
])->when(
$input->getArgument('stack') === 'inertia',
fn ($options) => $options->put('ssr', 'Inertia SSR')
)->sort()->all(),
))->each(fn ($option) => $input->setOption($option, true));
$input->setOption('pest', select(
label: 'Which testing framework do you prefer?',
options: ['Pest', 'PHPUnit'],
default: 'Pest',
) === 'Pest');
}
/**
* Determine whether the project is already using Pest.
*
* @return bool
*/
protected function isUsingPest()
{
return class_exists(\Pest\TestSuite::class);
}
}
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void add(\Illuminate\Foundation\Auth\User $user, \Illuminate\Database\Eloquent\Model $team, string $email, string $role = null)
*/
interface AddsTeamMembers
{
//
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method \Illuminate\Database\Eloquent\Model create(\Illuminate\Foundation\Auth\User $user, array $input)
*/
interface CreatesTeams
{
//
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void delete(\Illuminate\Database\Eloquent\Model $team)
*/
interface DeletesTeams
{
//
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void delete(\Illuminate\Foundation\Auth\User $user)
*/
interface DeletesUsers
{
//
}
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void invite(\Illuminate\Foundation\Auth\User $user, \Illuminate\Database\Eloquent\Model $team, string $email, string $role = null)
*/
interface InvitesTeamMembers
{
//
}
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void remove(\Illuminate\Foundation\Auth\User $user, \Illuminate\Database\Eloquent\Model $team, \Illuminate\Foundation\Auth\User $teamMember)
*/
interface RemovesTeamMembers
{
//
}
@@ -0,0 +1,11 @@
<?php
namespace Laravel\Jetstream\Contracts;
/**
* @method void update(\Illuminate\Foundation\Auth\User $user, \Illuminate\Database\Eloquent\Model $team, array $input)
*/
interface UpdatesTeamNames
{
//
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class AddingTeam
{
use Dispatchable;
/**
* The team owner.
*
* @var mixed
*/
public $owner;
/**
* Create a new event instance.
*
* @param mixed $owner
* @return void
*/
public function __construct($owner)
{
$this->owner = $owner;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class AddingTeamMember
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The team member being added.
*
* @var mixed
*/
public $user;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $user
* @return void
*/
public function __construct($team, $user)
{
$this->team = $team;
$this->user = $user;
}
}
@@ -0,0 +1,46 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class InvitingTeamMember
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The email address of the invitee.
*
* @var mixed
*/
public $email;
/**
* The role of the invitee.
*
* @var mixed
*/
public $role;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $email
* @param mixed $role
* @return void
*/
public function __construct($team, $email, $role)
{
$this->team = $team;
$this->email = $email;
$this->role = $role;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class RemovingTeamMember
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The team member being removed.
*
* @var mixed
*/
public $user;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $user
* @return void
*/
public function __construct($team, $user)
{
$this->team = $team;
$this->user = $user;
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Laravel\Jetstream\Events;
class TeamCreated extends TeamEvent
{
//
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Laravel\Jetstream\Events;
class TeamDeleted extends TeamEvent
{
//
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
abstract class TeamEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* The team instance.
*
* @var \App\Models\Team
*/
public $team;
/**
* Create a new event instance.
*
* @param \App\Models\Team $team
* @return void
*/
public function __construct($team)
{
$this->team = $team;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class TeamMemberAdded
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The team member that was added.
*
* @var mixed
*/
public $user;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $user
* @return void
*/
public function __construct($team, $user)
{
$this->team = $team;
$this->user = $user;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class TeamMemberRemoved
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The team member that was removed.
*
* @var mixed
*/
public $user;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $user
* @return void
*/
public function __construct($team, $user)
{
$this->team = $team;
$this->user = $user;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Laravel\Jetstream\Events;
use Illuminate\Foundation\Events\Dispatchable;
class TeamMemberUpdated
{
use Dispatchable;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The team member that was updated.
*
* @var mixed
*/
public $user;
/**
* Create a new event instance.
*
* @param mixed $team
* @param mixed $user
* @return void
*/
public function __construct($team, $user)
{
$this->team = $team;
$this->user = $user;
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Laravel\Jetstream\Events;
class TeamUpdated extends TeamEvent
{
//
}
+145
View File
@@ -0,0 +1,145 @@
<?php
namespace Laravel\Jetstream;
class Features
{
/**
* Determine if the given feature is enabled.
*
* @param string $feature
* @return bool
*/
public static function enabled(string $feature)
{
return in_array($feature, config('jetstream.features', []));
}
/**
* Determine if the feature is enabled and has a given option enabled.
*
* @param string $feature
* @param string $option
* @return bool
*/
public static function optionEnabled(string $feature, string $option)
{
return static::enabled($feature) &&
config("jetstream-options.{$feature}.{$option}") === true;
}
/**
* Determine if the application is allowing profile photo uploads.
*
* @return bool
*/
public static function managesProfilePhotos()
{
return static::enabled(static::profilePhotos());
}
/**
* Determine if the application is using any API features.
*
* @return bool
*/
public static function hasApiFeatures()
{
return static::enabled(static::api());
}
/**
* Determine if the application is using any team features.
*
* @return bool
*/
public static function hasTeamFeatures()
{
return static::enabled(static::teams());
}
/**
* Determine if invitations are sent to team members.
*
* @return bool
*/
public static function sendsTeamInvitations()
{
return static::optionEnabled(static::teams(), 'invitations');
}
/**
* Determine if the application has terms of service / privacy policy confirmation enabled.
*
* @return bool
*/
public static function hasTermsAndPrivacyPolicyFeature()
{
return static::enabled(static::termsAndPrivacyPolicy());
}
/**
* Determine if the application is using any account deletion features.
*
* @return bool
*/
public static function hasAccountDeletionFeatures()
{
return static::enabled(static::accountDeletion());
}
/**
* Enable the profile photo upload feature.
*
* @return string
*/
public static function profilePhotos()
{
return 'profile-photos';
}
/**
* Enable the API feature.
*
* @return string
*/
public static function api()
{
return 'api';
}
/**
* Enable the teams feature.
*
* @param array $options
* @return string
*/
public static function teams(array $options = [])
{
if (! empty($options)) {
config(['jetstream-options.teams' => $options]);
}
return 'teams';
}
/**
* Enable the terms of service and privacy policy feature.
*
* @return string
*/
public static function termsAndPrivacyPolicy()
{
return 'terms';
}
/**
* Enable the account deletion feature.
*
* @return string
*/
public static function accountDeletion()
{
return 'account-deletion';
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
trait HasProfilePhoto
{
/**
* Update the user's profile photo.
*
* @param \Illuminate\Http\UploadedFile $photo
* @param string $storagePath
* @return void
*/
public function updateProfilePhoto(UploadedFile $photo, $storagePath = 'profile-photos')
{
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
$this->forceFill([
'profile_photo_path' => $photo->storePublicly(
$storagePath, ['disk' => $this->profilePhotoDisk()]
),
])->save();
if ($previous) {
Storage::disk($this->profilePhotoDisk())->delete($previous);
}
});
}
/**
* Delete the user's profile photo.
*
* @return void
*/
public function deleteProfilePhoto()
{
if (! Features::managesProfilePhotos()) {
return;
}
if (is_null($this->profile_photo_path)) {
return;
}
Storage::disk($this->profilePhotoDisk())->delete($this->profile_photo_path);
$this->forceFill([
'profile_photo_path' => null,
])->save();
}
/**
* Get the URL to the user's profile photo.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function profilePhotoUrl(): Attribute
{
return Attribute::get(function (): string {
return $this->profile_photo_path
? Storage::disk($this->profilePhotoDisk())->url($this->profile_photo_path)
: $this->defaultProfilePhotoUrl();
});
}
/**
* Get the default profile photo URL if no profile photo has been uploaded.
*
* @return string
*/
protected function defaultProfilePhotoUrl()
{
$name = trim(collect(explode(' ', $this->name))->map(function ($segment) {
return mb_substr($segment, 0, 1);
})->join(' '));
return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=7F9CF5&background=EBF4FF';
}
/**
* Get the disk that profile photos should be stored on.
*
* @return string
*/
protected function profilePhotoDisk()
{
return isset($_ENV['VAPOR_ARTIFACT_NAME']) ? 's3' : config('jetstream.profile_photo_disk', 'public');
}
}
+223
View File
@@ -0,0 +1,223 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
trait HasTeams
{
/**
* Determine if the given team is the current team.
*
* @param mixed $team
* @return bool
*/
public function isCurrentTeam($team)
{
return $team->id === $this->currentTeam->id;
}
/**
* Get the current team of the user's context.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function currentTeam()
{
if (is_null($this->current_team_id) && $this->id) {
$this->switchTeam($this->personalTeam());
}
return $this->belongsTo(Jetstream::teamModel(), 'current_team_id');
}
/**
* Switch the user's context to the given team.
*
* @param mixed $team
* @return bool
*/
public function switchTeam($team)
{
if (! $this->belongsToTeam($team)) {
return false;
}
$this->forceFill([
'current_team_id' => $team->id,
])->save();
$this->setRelation('currentTeam', $team);
return true;
}
/**
* Get all of the teams the user owns or belongs to.
*
* @return \Illuminate\Support\Collection
*/
public function allTeams()
{
return $this->ownedTeams->merge($this->teams)->sortBy('name');
}
/**
* Get all of the teams the user owns.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function ownedTeams()
{
return $this->hasMany(Jetstream::teamModel());
}
/**
* Get all of the teams the user belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function teams()
{
return $this->belongsToMany(Jetstream::teamModel(), Jetstream::membershipModel())
->withPivot('role')
->withTimestamps()
->as('membership');
}
/**
* Get the user's "personal" team.
*
* @return \App\Models\Team
*/
public function personalTeam()
{
return $this->ownedTeams->where('personal_team', true)->first();
}
/**
* Determine if the user owns the given team.
*
* @param mixed $team
* @return bool
*/
public function ownsTeam($team)
{
if (is_null($team)) {
return false;
}
return $this->id == $team->{$this->getForeignKey()};
}
/**
* Determine if the user belongs to the given team.
*
* @param mixed $team
* @return bool
*/
public function belongsToTeam($team)
{
if (is_null($team)) {
return false;
}
return $this->ownsTeam($team) || $this->teams->contains(function ($t) use ($team) {
return $t->id === $team->id;
});
}
/**
* Get the role that the user has on the team.
*
* @param mixed $team
* @return \Laravel\Jetstream\Role|null
*/
public function teamRole($team)
{
if ($this->ownsTeam($team)) {
return new OwnerRole;
}
if (! $this->belongsToTeam($team)) {
return;
}
$role = $team->users
->where('id', $this->id)
->first()
->membership
->role;
return $role ? Jetstream::findRole($role) : null;
}
/**
* Determine if the user has the given role on the given team.
*
* @param mixed $team
* @param string $role
* @return bool
*/
public function hasTeamRole($team, string $role)
{
if ($this->ownsTeam($team)) {
return true;
}
return $this->belongsToTeam($team) && optional(Jetstream::findRole($team->users->where(
'id', $this->id
)->first()->membership->role))->key === $role;
}
/**
* Get the user's permissions for the given team.
*
* @param mixed $team
* @return array
*/
public function teamPermissions($team)
{
if ($this->ownsTeam($team)) {
return ['*'];
}
if (! $this->belongsToTeam($team)) {
return [];
}
return (array) optional($this->teamRole($team))->permissions;
}
/**
* Determine if the user has the given permission on the given team.
*
* @param mixed $team
* @param string $permission
* @return bool
*/
public function hasTeamPermission($team, string $permission)
{
if ($this->ownsTeam($team)) {
return true;
}
if (! $this->belongsToTeam($team)) {
return false;
}
if (in_array(HasApiTokens::class, class_uses_recursive($this)) &&
! $this->tokenCan($permission) &&
$this->currentAccessToken() !== null) {
return false;
}
$permissions = $this->teamPermissions($team);
return in_array($permission, $permissions) ||
in_array('*', $permissions) ||
(Str::endsWith($permission, ':create') && in_array('*:create', $permissions)) ||
(Str::endsWith($permission, ':update') && in_array('*:update', $permissions));
}
}
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Laravel\Jetstream\Jetstream;
class CurrentTeamController extends Controller
{
/**
* Update the authenticated user's current team.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function update(Request $request)
{
$team = Jetstream::newTeamModel()->findOrFail($request->team_id);
if (! $request->user()->switchTeam($team)) {
abort(403);
}
return redirect(config('fortify.home'), 303);
}
}
@@ -0,0 +1,88 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Laravel\Jetstream\Jetstream;
class ApiTokenController extends Controller
{
/**
* Show the user API token screen.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function index(Request $request)
{
return Jetstream::inertia()->render($request, 'API/Index', [
'tokens' => $request->user()->tokens->map(function ($token) {
return $token->toArray() + [
'last_used_ago' => optional($token->last_used_at)->diffForHumans(),
];
}),
'availablePermissions' => Jetstream::$permissions,
'defaultPermissions' => Jetstream::$defaultPermissions,
]);
}
/**
* Create a new API token.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
]);
$token = $request->user()->createToken(
$request->name,
Jetstream::validPermissions($request->input('permissions', []))
);
return back()->with('flash', [
'token' => explode('|', $token->plainTextToken, 2)[1],
]);
}
/**
* Update the given API token's permissions.
*
* @param \Illuminate\Http\Request $request
* @param string $tokenId
* @return \Illuminate\Http\RedirectResponse
*/
public function update(Request $request, $tokenId)
{
$request->validate([
'permissions' => 'array',
'permissions.*' => 'string',
]);
$token = $request->user()->tokens()->where('id', $tokenId)->firstOrFail();
$token->forceFill([
'abilities' => Jetstream::validPermissions($request->input('permissions', [])),
])->save();
return back(303);
}
/**
* Delete the given API token.
*
* @param \Illuminate\Http\Request $request
* @param string $tokenId
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request, $tokenId)
{
$request->user()->tokens()->where('id', $tokenId)->first()->delete();
return back(303);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia\Concerns;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features;
trait ConfirmsTwoFactorAuthentication
{
/**
* Validate the two factor authentication state for the request.
*
* @param \Illuminate\Http\Request
* @return void
*/
protected function validateTwoFactorAuthenticationState(Request $request)
{
if (! Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
return;
}
$currentTime = time();
// Notate totally disabled state in session...
if ($this->twoFactorAuthenticationDisabled($request)) {
$request->session()->put('two_factor_empty_at', $currentTime);
}
// If was previously totally disabled this session but is now confirming, notate time...
if ($this->hasJustBegunConfirmingTwoFactorAuthentication($request)) {
$request->session()->put('two_factor_confirming_at', $currentTime);
}
// If the profile is reloaded and is not confirmed but was previously in confirming state, disable...
if ($this->neverFinishedConfirmingTwoFactorAuthentication($request, $currentTime)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());
$request->session()->put('two_factor_empty_at', $currentTime);
$request->session()->remove('two_factor_confirming_at');
}
}
/**
* Determine if two factor authenticatoin is totally disabled.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function twoFactorAuthenticationDisabled(Request $request)
{
return is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at);
}
/**
* Determine if two factor authentication is just now being confirmed within the last request cycle.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasJustBegunConfirmingTwoFactorAuthentication(Request $request)
{
return ! is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->has('two_factor_empty_at') &&
is_null($request->session()->get('two_factor_confirming_at'));
}
/**
* Determine if two factor authentication was never totally confirmed once confirmation started.
*
* @param \Illuminate\Http\Request $request
* @param int $currentTime
* @return bool
*/
protected function neverFinishedConfirmingTwoFactorAuthentication(Request $request, $currentTime)
{
return ! array_key_exists('code', $request->session()->getOldInput()) &&
is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->get('two_factor_confirming_at', 0) != $currentTime;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Laravel\Fortify\Actions\ConfirmPassword;
use Laravel\Jetstream\Contracts\DeletesUsers;
class CurrentUserController extends Controller
{
/**
* Delete the current user.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\StatefulGuard $guard
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request, StatefulGuard $guard)
{
$confirmed = app(ConfirmPassword::class)(
$guard, $request->user(), $request->password
);
if (! $confirmed) {
throw ValidationException::withMessages([
'password' => __('The password is incorrect.'),
]);
}
app(DeletesUsers::class)->delete($request->user()->fresh());
$guard->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Inertia::location(url('/'));
}
}
@@ -0,0 +1,57 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Actions\ConfirmPassword;
class OtherBrowserSessionsController extends Controller
{
/**
* Log out from other browser sessions.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\StatefulGuard $guard
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request, StatefulGuard $guard)
{
$confirmed = app(ConfirmPassword::class)(
$guard, $request->user(), $request->password
);
if (! $confirmed) {
throw ValidationException::withMessages([
'password' => __('The password is incorrect.'),
]);
}
$guard->logoutOtherDevices($request->password);
$this->deleteOtherSessionRecords($request);
return back(303);
}
/**
* Delete the other browser session records from storage.
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function deleteOtherSessionRecords(Request $request)
{
if (config('session.driver') !== 'database') {
return;
}
DB::connection(config('session.connection'))->table(config('session.table', 'sessions'))
->where('user_id', $request->user()->getAuthIdentifier())
->where('id', '!=', $request->session()->getId())
->delete();
}
}
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Laravel\Jetstream\Jetstream;
class PrivacyPolicyController extends Controller
{
/**
* Show the privacy policy for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function show(Request $request)
{
$policyFile = Jetstream::localizedMarkdownPath('policy.md');
return Inertia::render('PrivacyPolicy', [
'policy' => Str::markdown(file_get_contents($policyFile)),
]);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class ProfilePhotoController extends Controller
{
/**
* Delete the current user's profile photo.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request)
{
$request->user()->deleteProfilePhoto();
return back(303)->with('status', 'profile-photo-deleted');
}
}
@@ -0,0 +1,110 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Laravel\Jetstream\Actions\ValidateTeamDeletion;
use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\Contracts\DeletesTeams;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\RedirectsActions;
class TeamController extends Controller
{
use RedirectsActions;
/**
* Show the team management screen.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @return \Inertia\Response
*/
public function show(Request $request, $teamId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
Gate::authorize('view', $team);
return Jetstream::inertia()->render($request, 'Teams/Show', [
'team' => $team->load('owner', 'users', 'teamInvitations'),
'availableRoles' => array_values(Jetstream::$roles),
'availablePermissions' => Jetstream::$permissions,
'defaultPermissions' => Jetstream::$defaultPermissions,
'permissions' => [
'canAddTeamMembers' => Gate::check('addTeamMember', $team),
'canDeleteTeam' => Gate::check('delete', $team),
'canRemoveTeamMembers' => Gate::check('removeTeamMember', $team),
'canUpdateTeam' => Gate::check('update', $team),
'canUpdateTeamMembers' => Gate::check('updateTeamMember', $team),
],
]);
}
/**
* Show the team creation screen.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function create(Request $request)
{
Gate::authorize('create', Jetstream::newTeamModel());
return Jetstream::inertia()->render($request, 'Teams/Create');
}
/**
* Create a new team.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request)
{
$creator = app(CreatesTeams::class);
$creator->create($request->user(), $request->all());
return $this->redirectPath($creator);
}
/**
* Update the given team's name.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @return \Illuminate\Http\RedirectResponse
*/
public function update(Request $request, $teamId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
app(UpdatesTeamNames::class)->update($request->user(), $team, $request->all());
return back(303);
}
/**
* Delete the given team.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request, $teamId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
app(ValidateTeamDeletion::class)->validate($request->user(), $team);
$deleter = app(DeletesTeams::class);
$deleter->delete($team);
return $this->redirectPath($deleter);
}
}
@@ -0,0 +1,90 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Laravel\Jetstream\Actions\UpdateTeamMemberRole;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Features;
use Laravel\Jetstream\Jetstream;
class TeamMemberController extends Controller
{
/**
* Add a new team member to a team.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request, $teamId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
if (Features::sendsTeamInvitations()) {
app(InvitesTeamMembers::class)->invite(
$request->user(),
$team,
$request->email ?: '',
$request->role
);
} else {
app(AddsTeamMembers::class)->add(
$request->user(),
$team,
$request->email ?: '',
$request->role
);
}
return back(303);
}
/**
* Update the given team member's role.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @param int $userId
* @return \Illuminate\Http\RedirectResponse
*/
public function update(Request $request, $teamId, $userId)
{
app(UpdateTeamMemberRole::class)->update(
$request->user(),
Jetstream::newTeamModel()->findOrFail($teamId),
$userId,
$request->role
);
return back(303);
}
/**
* Remove the given user from the given team.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @param int $userId
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request, $teamId, $userId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
app(RemovesTeamMembers::class)->remove(
$request->user(),
$team,
$user = Jetstream::findUserByIdOrFail($userId)
);
if ($request->user()->id === $user->id) {
return redirect(config('fortify.home'));
}
return back(303);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Laravel\Jetstream\Jetstream;
class TermsOfServiceController extends Controller
{
/**
* Show the terms of service for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function show(Request $request)
{
$termsFile = Jetstream::localizedMarkdownPath('terms.md');
return Inertia::render('TermsOfService', [
'terms' => Str::markdown(file_get_contents($termsFile)),
]);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Inertia;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Laravel\Fortify\Features;
use Laravel\Jetstream\Agent;
use Laravel\Jetstream\Jetstream;
class UserProfileController extends Controller
{
use Concerns\ConfirmsTwoFactorAuthentication;
/**
* Show the general profile settings screen.
*
* @param \Illuminate\Http\Request $request
* @return \Inertia\Response
*/
public function show(Request $request)
{
$this->validateTwoFactorAuthenticationState($request);
return Jetstream::inertia()->render($request, 'Profile/Show', [
'confirmsTwoFactorAuthentication' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'sessions' => $this->sessions($request)->all(),
]);
}
/**
* Get the current sessions.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Support\Collection
*/
public function sessions(Request $request)
{
if (config('session.driver') !== 'database') {
return collect();
}
return collect(
DB::connection(config('session.connection'))->table(config('session.table', 'sessions'))
->where('user_id', $request->user()->getAuthIdentifier())
->orderBy('last_activity', 'desc')
->get()
)->map(function ($session) use ($request) {
$agent = $this->createAgent($session);
return (object) [
'agent' => [
'is_desktop' => $agent->isDesktop(),
'platform' => $agent->platform(),
'browser' => $agent->browser(),
],
'ip_address' => $session->ip_address,
'is_current_device' => $session->id === $request->session()->getId(),
'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
];
});
}
/**
* Create a new agent instance from the given session.
*
* @param mixed $session
* @return \Laravel\Jetstream\Agent
*/
protected function createAgent($session)
{
return tap(new Agent(), fn ($agent) => $agent->setUserAgent($session->user_agent));
}
}
@@ -0,0 +1,23 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Livewire;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class ApiTokenController extends Controller
{
/**
* Show the user API token screen.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request)
{
return view('api.index', [
'request' => $request,
'user' => $request->user(),
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Livewire;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Str;
use Laravel\Jetstream\Jetstream;
class PrivacyPolicyController extends Controller
{
/**
* Show the privacy policy for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function show(Request $request)
{
$policyFile = Jetstream::localizedMarkdownPath('policy.md');
return view('policy', [
'policy' => Str::markdown(file_get_contents($policyFile)),
]);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Livewire;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Laravel\Jetstream\Jetstream;
class TeamController extends Controller
{
/**
* Show the team management screen.
*
* @param \Illuminate\Http\Request $request
* @param int $teamId
* @return \Illuminate\View\View
*/
public function show(Request $request, $teamId)
{
$team = Jetstream::newTeamModel()->findOrFail($teamId);
if (Gate::denies('view', $team)) {
abort(403);
}
return view('teams.show', [
'user' => $request->user(),
'team' => $team,
]);
}
/**
* Show the team creation screen.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function create(Request $request)
{
Gate::authorize('create', Jetstream::newTeamModel());
return view('teams.create', [
'user' => $request->user(),
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Livewire;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Str;
use Laravel\Jetstream\Jetstream;
class TermsOfServiceController extends Controller
{
/**
* Show the terms of service for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function show(Request $request)
{
$termsFile = Jetstream::localizedMarkdownPath('terms.md');
return view('terms', [
'terms' => Str::markdown(file_get_contents($termsFile)),
]);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Laravel\Jetstream\Http\Controllers\Livewire;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class UserProfileController extends Controller
{
/**
* Show the user profile screen.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function show(Request $request)
{
return view('profile.show', [
'request' => $request,
'user' => $request->user(),
]);
}
}
@@ -0,0 +1,62 @@
<?php
namespace Laravel\Jetstream\Http\Controllers;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Gate;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Jetstream;
class TeamInvitationController extends Controller
{
/**
* Accept a team invitation.
*
* @param \Illuminate\Http\Request $request
* @param int $invitationId
* @return \Illuminate\Http\RedirectResponse
*/
public function accept(Request $request, $invitationId)
{
$model = Jetstream::teamInvitationModel();
$invitation = $model::whereKey($invitationId)->firstOrFail();
app(AddsTeamMembers::class)->add(
$invitation->team->owner,
$invitation->team,
$invitation->email,
$invitation->role
);
$invitation->delete();
return redirect(config('fortify.home'))->banner(
__('Great! You have accepted the invitation to join the :team team.', ['team' => $invitation->team->name]),
);
}
/**
* Cancel the given team invitation.
*
* @param \Illuminate\Http\Request $request
* @param int $invitationId
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request, $invitationId)
{
$model = Jetstream::teamInvitationModel();
$invitation = $model::whereKey($invitationId)->firstOrFail();
if (! Gate::forUser($request->user())->check('removeTeamMember', $invitation->team)) {
throw new AuthorizationException;
}
$invitation->delete();
return back(303);
}
}
@@ -0,0 +1,203 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Jetstream;
use Livewire\Component;
class ApiTokenManager extends Component
{
/**
* The create API token form state.
*
* @var array
*/
public $createApiTokenForm = [
'name' => '',
'permissions' => [],
];
/**
* Indicates if the plain text token is being displayed to the user.
*
* @var bool
*/
public $displayingToken = false;
/**
* The plain text token value.
*
* @var string|null
*/
public $plainTextToken;
/**
* Indicates if the user is currently managing an API token's permissions.
*
* @var bool
*/
public $managingApiTokenPermissions = false;
/**
* The token that is currently having its permissions managed.
*
* @var \Laravel\Sanctum\PersonalAccessToken|null
*/
public $managingPermissionsFor;
/**
* The update API token form state.
*
* @var array
*/
public $updateApiTokenForm = [
'permissions' => [],
];
/**
* Indicates if the application is confirming if an API token should be deleted.
*
* @var bool
*/
public $confirmingApiTokenDeletion = false;
/**
* The ID of the API token being deleted.
*
* @var int
*/
public $apiTokenIdBeingDeleted;
/**
* Mount the component.
*
* @return void
*/
public function mount()
{
$this->createApiTokenForm['permissions'] = Jetstream::$defaultPermissions;
}
/**
* Create a new API token.
*
* @return void
*/
public function createApiToken()
{
$this->resetErrorBag();
Validator::make([
'name' => $this->createApiTokenForm['name'],
], [
'name' => ['required', 'string', 'max:255'],
])->validateWithBag('createApiToken');
$this->displayTokenValue($this->user->createToken(
$this->createApiTokenForm['name'],
Jetstream::validPermissions($this->createApiTokenForm['permissions'])
));
$this->createApiTokenForm['name'] = '';
$this->createApiTokenForm['permissions'] = Jetstream::$defaultPermissions;
$this->dispatch('created');
}
/**
* Display the token value to the user.
*
* @param \Laravel\Sanctum\NewAccessToken $token
* @return void
*/
protected function displayTokenValue($token)
{
$this->displayingToken = true;
$this->plainTextToken = explode('|', $token->plainTextToken, 2)[1];
$this->dispatch('showing-token-modal');
}
/**
* Allow the given token's permissions to be managed.
*
* @param int $tokenId
* @return void
*/
public function manageApiTokenPermissions($tokenId)
{
$this->managingApiTokenPermissions = true;
$this->managingPermissionsFor = $this->user->tokens()->where(
'id', $tokenId
)->firstOrFail();
$this->updateApiTokenForm['permissions'] = $this->managingPermissionsFor->abilities;
}
/**
* Update the API token's permissions.
*
* @return void
*/
public function updateApiToken()
{
$this->managingPermissionsFor->forceFill([
'abilities' => Jetstream::validPermissions($this->updateApiTokenForm['permissions']),
])->save();
$this->managingApiTokenPermissions = false;
}
/**
* Confirm that the given API token should be deleted.
*
* @param int $tokenId
* @return void
*/
public function confirmApiTokenDeletion($tokenId)
{
$this->confirmingApiTokenDeletion = true;
$this->apiTokenIdBeingDeleted = $tokenId;
}
/**
* Delete the API token.
*
* @return void
*/
public function deleteApiToken()
{
$this->user->tokens()->where('id', $this->apiTokenIdBeingDeleted)->first()->delete();
$this->user->load('tokens');
$this->confirmingApiTokenDeletion = false;
$this->managingPermissionsFor = null;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('api.api-token-manager');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\RedirectsActions;
use Livewire\Component;
class CreateTeamForm extends Component
{
use RedirectsActions;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* Create a new team.
*
* @param \Laravel\Jetstream\Contracts\CreatesTeams $creator
* @return mixed
*/
public function createTeam(CreatesTeams $creator)
{
$this->resetErrorBag();
$creator->create(Auth::user(), $this->state);
return $this->redirectPath($creator);
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('teams.create-team-form');
}
}
@@ -0,0 +1,67 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Jetstream\Actions\ValidateTeamDeletion;
use Laravel\Jetstream\Contracts\DeletesTeams;
use Laravel\Jetstream\RedirectsActions;
use Livewire\Component;
class DeleteTeamForm extends Component
{
use RedirectsActions;
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* Indicates if team deletion is being confirmed.
*
* @var bool
*/
public $confirmingTeamDeletion = false;
/**
* Mount the component.
*
* @param mixed $team
* @return void
*/
public function mount($team)
{
$this->team = $team;
}
/**
* Delete the team.
*
* @param \Laravel\Jetstream\Actions\ValidateTeamDeletion $validator
* @param \Laravel\Jetstream\Contracts\DeletesTeams $deleter
* @return mixed
*/
public function deleteTeam(ValidateTeamDeletion $validator, DeletesTeams $deleter)
{
$validator->validate(Auth::user(), $this->team);
$deleter->delete($this->team);
$this->team = null;
return $this->redirectPath($deleter);
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('teams.delete-team-form');
}
}
@@ -0,0 +1,84 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\DeletesUsers;
use Livewire\Component;
class DeleteUserForm extends Component
{
/**
* Indicates if user deletion is being confirmed.
*
* @var bool
*/
public $confirmingUserDeletion = false;
/**
* The user's current password.
*
* @var string
*/
public $password = '';
/**
* Confirm that the user would like to delete their account.
*
* @return void
*/
public function confirmUserDeletion()
{
$this->resetErrorBag();
$this->password = '';
$this->dispatch('confirming-delete-user');
$this->confirmingUserDeletion = true;
}
/**
* Delete the current user.
*
* @param \Illuminate\Http\Request $request
* @param \Laravel\Jetstream\Contracts\DeletesUsers $deleter
* @param \Illuminate\Contracts\Auth\StatefulGuard $auth
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function deleteUser(Request $request, DeletesUsers $deleter, StatefulGuard $auth)
{
$this->resetErrorBag();
if (! Hash::check($this->password, Auth::user()->password)) {
throw ValidationException::withMessages([
'password' => [__('This password does not match our records.')],
]);
}
$deleter->delete(Auth::user()->fresh());
$auth->logout();
if ($request->hasSession()) {
$request->session()->invalidate();
$request->session()->regenerateToken();
}
return redirect(config('fortify.redirects.logout') ?? '/');
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('profile.delete-user-form');
}
}
@@ -0,0 +1,140 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Agent;
use Livewire\Component;
class LogoutOtherBrowserSessionsForm extends Component
{
/**
* Indicates if logout is being confirmed.
*
* @var bool
*/
public $confirmingLogout = false;
/**
* The user's current password.
*
* @var string
*/
public $password = '';
/**
* Confirm that the user would like to log out from other browser sessions.
*
* @return void
*/
public function confirmLogout()
{
$this->password = '';
$this->dispatch('confirming-logout-other-browser-sessions');
$this->confirmingLogout = true;
}
/**
* Log out from other browser sessions.
*
* @param \Illuminate\Contracts\Auth\StatefulGuard $guard
* @return void
*/
public function logoutOtherBrowserSessions(StatefulGuard $guard)
{
if (config('session.driver') !== 'database') {
return;
}
$this->resetErrorBag();
if (! Hash::check($this->password, Auth::user()->password)) {
throw ValidationException::withMessages([
'password' => [__('This password does not match our records.')],
]);
}
$guard->logoutOtherDevices($this->password);
$this->deleteOtherSessionRecords();
request()->session()->put([
'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
]);
$this->confirmingLogout = false;
$this->dispatch('loggedOut');
}
/**
* Delete the other browser session records from storage.
*
* @return void
*/
protected function deleteOtherSessionRecords()
{
if (config('session.driver') !== 'database') {
return;
}
DB::connection(config('session.connection'))->table(config('session.table', 'sessions'))
->where('user_id', Auth::user()->getAuthIdentifier())
->where('id', '!=', request()->session()->getId())
->delete();
}
/**
* Get the current sessions.
*
* @return \Illuminate\Support\Collection
*/
public function getSessionsProperty()
{
if (config('session.driver') !== 'database') {
return collect();
}
return collect(
DB::connection(config('session.connection'))->table(config('session.table', 'sessions'))
->where('user_id', Auth::user()->getAuthIdentifier())
->orderBy('last_activity', 'desc')
->get()
)->map(function ($session) {
return (object) [
'agent' => $this->createAgent($session),
'ip_address' => $session->ip_address,
'is_current_device' => $session->id === request()->session()->getId(),
'last_active' => Carbon::createFromTimestamp($session->last_activity)->diffForHumans(),
];
});
}
/**
* Create a new agent instance from the given session.
*
* @param mixed $session
* @return \Laravel\Jetstream\Agent
*/
protected function createAgent($session)
{
return tap(new Agent(), fn ($agent) => $agent->setUserAgent($session->user_agent));
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('profile.logout-other-browser-sessions-form');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Livewire\Component;
class NavigationMenu extends Component
{
/**
* The component's listeners.
*
* @var array
*/
protected $listeners = [
'refresh-navigation-menu' => '$refresh',
];
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('navigation-menu');
}
}
@@ -0,0 +1,276 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Jetstream\Actions\UpdateTeamMemberRole;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Features;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Role;
use Livewire\Component;
class TeamMemberManager extends Component
{
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* Indicates if a user's role is currently being managed.
*
* @var bool
*/
public $currentlyManagingRole = false;
/**
* The user that is having their role managed.
*
* @var mixed
*/
public $managingRoleFor;
/**
* The current role for the user that is having their role managed.
*
* @var string
*/
public $currentRole;
/**
* Indicates if the application is confirming if a user wishes to leave the current team.
*
* @var bool
*/
public $confirmingLeavingTeam = false;
/**
* Indicates if the application is confirming if a team member should be removed.
*
* @var bool
*/
public $confirmingTeamMemberRemoval = false;
/**
* The ID of the team member being removed.
*
* @var int|null
*/
public $teamMemberIdBeingRemoved = null;
/**
* The "add team member" form state.
*
* @var array
*/
public $addTeamMemberForm = [
'email' => '',
'role' => null,
];
/**
* Mount the component.
*
* @param mixed $team
* @return void
*/
public function mount($team)
{
$this->team = $team;
}
/**
* Add a new team member to a team.
*
* @return void
*/
public function addTeamMember()
{
$this->resetErrorBag();
if (Features::sendsTeamInvitations()) {
app(InvitesTeamMembers::class)->invite(
$this->user,
$this->team,
$this->addTeamMemberForm['email'],
$this->addTeamMemberForm['role']
);
} else {
app(AddsTeamMembers::class)->add(
$this->user,
$this->team,
$this->addTeamMemberForm['email'],
$this->addTeamMemberForm['role']
);
}
$this->addTeamMemberForm = [
'email' => '',
'role' => null,
];
$this->team = $this->team->fresh();
$this->dispatch('saved');
}
/**
* Cancel a pending team member invitation.
*
* @param int $invitationId
* @return void
*/
public function cancelTeamInvitation($invitationId)
{
if (! empty($invitationId)) {
$model = Jetstream::teamInvitationModel();
$model::whereKey($invitationId)
->where('team_id', $this->team->id)
->delete();
}
$this->team = $this->team->fresh();
}
/**
* Allow the given user's role to be managed.
*
* @param int $userId
* @return void
*/
public function manageRole($userId)
{
$this->currentlyManagingRole = true;
$this->managingRoleFor = Jetstream::findUserByIdOrFail($userId);
$this->currentRole = $this->managingRoleFor->teamRole($this->team)->key;
}
/**
* Save the role for the user being managed.
*
* @param \Laravel\Jetstream\Actions\UpdateTeamMemberRole $updater
* @return void
*/
public function updateRole(UpdateTeamMemberRole $updater)
{
$updater->update(
$this->user,
$this->team,
$this->managingRoleFor->id,
$this->currentRole
);
$this->team = $this->team->fresh();
$this->stopManagingRole();
}
/**
* Stop managing the role of a given user.
*
* @return void
*/
public function stopManagingRole()
{
$this->currentlyManagingRole = false;
}
/**
* Remove the currently authenticated user from the team.
*
* @param \Laravel\Jetstream\Contracts\RemovesTeamMembers $remover
* @return \Illuminate\Http\RedirectResponse
*/
public function leaveTeam(RemovesTeamMembers $remover)
{
$remover->remove(
$this->user,
$this->team,
$this->user
);
$this->confirmingLeavingTeam = false;
$this->team = $this->team->fresh();
return redirect(config('fortify.home'));
}
/**
* Confirm that the given team member should be removed.
*
* @param int $userId
* @return void
*/
public function confirmTeamMemberRemoval($userId)
{
$this->confirmingTeamMemberRemoval = true;
$this->teamMemberIdBeingRemoved = $userId;
}
/**
* Remove a team member from the team.
*
* @param \Laravel\Jetstream\Contracts\RemovesTeamMembers $remover
* @return void
*/
public function removeTeamMember(RemovesTeamMembers $remover)
{
$remover->remove(
$this->user,
$this->team,
$user = Jetstream::findUserByIdOrFail($this->teamMemberIdBeingRemoved)
);
$this->confirmingTeamMemberRemoval = false;
$this->teamMemberIdBeingRemoved = null;
$this->team = $this->team->fresh();
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Get the available team member roles.
*
* @return array
*/
public function getRolesProperty()
{
return collect(Jetstream::$roles)->transform(function ($role) {
return with($role->jsonSerialize(), function ($data) {
return (new Role(
$data['key'],
$data['name'],
$data['permissions']
))->description($data['description']);
});
})->values()->all();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('teams.team-member-manager');
}
}
@@ -0,0 +1,180 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Fortify\Features;
use Laravel\Jetstream\ConfirmsPasswords;
use Livewire\Component;
class TwoFactorAuthenticationForm extends Component
{
use ConfirmsPasswords;
/**
* Indicates if two factor authentication QR code is being displayed.
*
* @var bool
*/
public $showingQrCode = false;
/**
* Indicates if the two factor authentication confirmation input and button are being displayed.
*
* @var bool
*/
public $showingConfirmation = false;
/**
* Indicates if two factor authentication recovery codes are being displayed.
*
* @var bool
*/
public $showingRecoveryCodes = false;
/**
* The OTP code for confirming two factor authentication.
*
* @var string|null
*/
public $code;
/**
* Mount the component.
*
* @return void
*/
public function mount()
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
is_null(Auth::user()->two_factor_confirmed_at)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());
}
}
/**
* Enable two factor authentication for the user.
*
* @param \Laravel\Fortify\Actions\EnableTwoFactorAuthentication $enable
* @return void
*/
public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$enable(Auth::user());
$this->showingQrCode = true;
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
$this->showingConfirmation = true;
} else {
$this->showingRecoveryCodes = true;
}
}
/**
* Confirm two factor authentication for the user.
*
* @param \Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication $confirm
* @return void
*/
public function confirmTwoFactorAuthentication(ConfirmTwoFactorAuthentication $confirm)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$confirm(Auth::user(), $this->code);
$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = true;
}
/**
* Display the user's recovery codes.
*
* @return void
*/
public function showRecoveryCodes()
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$this->showingRecoveryCodes = true;
}
/**
* Generate new recovery codes for the user.
*
* @param \Laravel\Fortify\Actions\GenerateNewRecoveryCodes $generate
* @return void
*/
public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$generate(Auth::user());
$this->showingRecoveryCodes = true;
}
/**
* Disable two factor authentication for the user.
*
* @param \Laravel\Fortify\Actions\DisableTwoFactorAuthentication $disable
* @return void
*/
public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
$disable(Auth::user());
$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = false;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Determine if two factor authentication is enabled.
*
* @return bool
*/
public function getEnabledProperty()
{
return ! empty($this->user->two_factor_secret);
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('profile.two-factor-authentication-form');
}
}
@@ -0,0 +1,68 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
use Livewire\Component;
class UpdatePasswordForm extends Component
{
/**
* The component's state.
*
* @var array
*/
public $state = [
'current_password' => '',
'password' => '',
'password_confirmation' => '',
];
/**
* Update the user's password.
*
* @param \Laravel\Fortify\Contracts\UpdatesUserPasswords $updater
* @return void
*/
public function updatePassword(UpdatesUserPasswords $updater)
{
$this->resetErrorBag();
$updater->update(Auth::user(), $this->state);
if (request()->hasSession()) {
request()->session()->put([
'password_hash_'.Auth::getDefaultDriver() => Auth::user()->getAuthPassword(),
]);
}
$this->state = [
'current_password' => '',
'password' => '',
'password_confirmation' => '',
];
$this->dispatch('saved');
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('profile.update-password-form');
}
}
@@ -0,0 +1,118 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class UpdateProfileInformationForm extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->state = array_merge([
'email' => $user->email,
], $user->withoutRelations()->toArray());
}
/**
* Update the user's profile information.
*
* @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater
* @return \Illuminate\Http\RedirectResponse|null
*/
public function updateProfileInformation(UpdatesUserProfileInformation $updater)
{
$this->resetErrorBag();
$updater->update(
Auth::user(),
$this->photo
? array_merge($this->state, ['photo' => $this->photo])
: $this->state
);
if (isset($this->photo)) {
return redirect()->route('profile.show');
}
$this->dispatch('saved');
$this->dispatch('refresh-navigation-menu');
}
/**
* Delete user's profile photo.
*
* @return void
*/
public function deleteProfilePhoto()
{
Auth::user()->deleteProfilePhoto();
$this->dispatch('refresh-navigation-menu');
}
/**
* Sent the email verification.
*
* @return void
*/
public function sendEmailVerification()
{
Auth::user()->sendEmailVerificationNotification();
$this->verificationLinkSent = true;
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('profile.update-profile-information-form');
}
}
@@ -0,0 +1,74 @@
<?php
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
use Livewire\Component;
class UpdateTeamNameForm extends Component
{
/**
* The team instance.
*
* @var mixed
*/
public $team;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* Mount the component.
*
* @param mixed $team
* @return void
*/
public function mount($team)
{
$this->team = $team;
$this->state = $team->withoutRelations()->toArray();
}
/**
* Update the team's name.
*
* @param \Laravel\Jetstream\Contracts\UpdatesTeamNames $updater
* @return void
*/
public function updateTeamName(UpdatesTeamNames $updater)
{
$this->resetErrorBag();
$updater->update($this->user, $this->team, $this->state);
$this->dispatch('saved');
$this->dispatch('refresh-navigation-menu');
}
/**
* Get the current user of the application.
*
* @return mixed
*/
public function getUserProperty()
{
return Auth::user();
}
/**
* Render the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('teams.update-team-name-form');
}
}
@@ -0,0 +1,19 @@
<?php
namespace Laravel\Jetstream\Http\Middleware;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Session\Middleware\AuthenticateSession as BaseAuthenticateSession;
class AuthenticateSession extends BaseAuthenticateSession
{
/**
* Get the guard instance that should be used by the middleware.
*
* @return \Illuminate\Contracts\Auth\Factory|\Illuminate\Contracts\Auth\Guard
*/
protected function guard()
{
return app(StatefulGuard::class);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Laravel\Jetstream\Http\Middleware;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Session;
use Inertia\Inertia;
use Laravel\Fortify\Features;
use Laravel\Jetstream\Jetstream;
class ShareInertiaData
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
Inertia::share(array_filter([
'jetstream' => function () use ($request) {
$user = $request->user();
return [
'canCreateTeams' => $user &&
Jetstream::userHasTeamFeatures($user) &&
Gate::forUser($user)->check('create', Jetstream::newTeamModel()),
'canManageTwoFactorAuthentication' => Features::canManageTwoFactorAuthentication(),
'canUpdatePassword' => Features::enabled(Features::updatePasswords()),
'canUpdateProfileInformation' => Features::canUpdateProfileInformation(),
'hasEmailVerification' => Features::enabled(Features::emailVerification()),
'flash' => $request->session()->get('flash', []),
'hasAccountDeletionFeatures' => Jetstream::hasAccountDeletionFeatures(),
'hasApiFeatures' => Jetstream::hasApiFeatures(),
'hasTeamFeatures' => Jetstream::hasTeamFeatures(),
'hasTermsAndPrivacyPolicyFeature' => Jetstream::hasTermsAndPrivacyPolicyFeature(),
'managesProfilePhotos' => Jetstream::managesProfilePhotos(),
];
},
'auth' => [
'user' => function () use ($request) {
if (! $user = $request->user()) {
return;
}
$userHasTeamFeatures = Jetstream::userHasTeamFeatures($user);
if ($user && $userHasTeamFeatures) {
$user->currentTeam;
}
return array_merge($user->toArray(), array_filter([
'all_teams' => $userHasTeamFeatures ? $user->allTeams()->values() : null,
]), [
'two_factor_enabled' => Features::enabled(Features::twoFactorAuthentication())
&& ! is_null($user->two_factor_secret),
]);
},
],
'errorBags' => function () {
return collect(optional(Session::get('errors'))->getBags() ?: [])->mapWithKeys(function ($bag, $key) {
return [$key => $bag->messages()];
})->all();
},
]));
return $next($request);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Http\Request;
use Inertia\Inertia;
class InertiaManager
{
/**
* The registered rendering callbacks.
*
* @var array
*/
protected $renderingCallbacks = [];
/**
* Render the given Inertia page.
*
* @param \Illuminate\Http\Request $request
* @param string $page
* @param array $data
* @return \Inertia\Response
*/
public function render(Request $request, string $page, array $data = [])
{
if (isset($this->renderingCallbacks[$page])) {
foreach ($this->renderingCallbacks[$page] as $callback) {
$data = $callback($request, $data);
}
}
return Inertia::render($page, $data);
}
/**
* Register a rendering callback.
*
* @param string $page
* @param callable $callback
* @return $this
*/
public function whenRendering(string $page, callable $callback)
{
$this->renderingCallbacks[$page][] = $callback;
return $this;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace Laravel\Jetstream;
trait InteractsWithBanner
{
/**
* Update the banner message.
*
* @param string $message
* @return void
*/
protected function banner($message)
{
$this->dispatch('banner-message',
style: 'success',
message: $message,
);
}
/**
* Update the banner message with a warning message.
*
* @param string $message
* @return void
*/
protected function warningBanner($message)
{
$this->dispatch('banner-message',
style: 'warning',
message: $message,
);
}
/**
* Update the banner message with a danger / error message.
*
* @param string $message
* @return void
*/
protected function dangerBanner($message)
{
$this->dispatch('banner-message',
style: 'danger',
message: $message,
);
}
}
+490
View File
@@ -0,0 +1,490 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\Contracts\DeletesTeams;
use Laravel\Jetstream\Contracts\DeletesUsers;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
class Jetstream
{
/**
* Indicates if Jetstream routes will be registered.
*
* @var bool
*/
public static $registersRoutes = true;
/**
* The roles that are available to assign to users.
*
* @var array
*/
public static $roles = [];
/**
* The permissions that exist within the application.
*
* @var array
*/
public static $permissions = [];
/**
* The default permissions that should be available to new entities.
*
* @var array
*/
public static $defaultPermissions = [];
/**
* The user model that should be used by Jetstream.
*
* @var string
*/
public static $userModel = 'App\\Models\\User';
/**
* The team model that should be used by Jetstream.
*
* @var string
*/
public static $teamModel = 'App\\Models\\Team';
/**
* The membership model that should be used by Jetstream.
*
* @var string
*/
public static $membershipModel = 'App\\Models\\Membership';
/**
* The team invitation model that should be used by Jetstream.
*
* @var string
*/
public static $teamInvitationModel = 'App\\Models\\TeamInvitation';
/**
* The Inertia manager instance.
*
* @var \Laravel\Jetstream\InertiaManager
*/
public static $inertiaManager;
/**
* Determine if Jetstream has registered roles.
*
* @return bool
*/
public static function hasRoles()
{
return count(static::$roles) > 0;
}
/**
* Find the role with the given key.
*
* @param string $key
* @return \Laravel\Jetstream\Role|null
*/
public static function findRole(string $key)
{
return static::$roles[$key] ?? null;
}
/**
* Define a role.
*
* @param string $key
* @param string $name
* @param array $permissions
* @return \Laravel\Jetstream\Role
*/
public static function role(string $key, string $name, array $permissions)
{
static::$permissions = collect(array_merge(static::$permissions, $permissions))
->unique()
->sort()
->values()
->all();
return tap(new Role($key, $name, $permissions), function ($role) use ($key) {
static::$roles[$key] = $role;
});
}
/**
* Determine if any permissions have been registered with Jetstream.
*
* @return bool
*/
public static function hasPermissions()
{
return count(static::$permissions) > 0;
}
/**
* Define the available API token permissions.
*
* @param array $permissions
* @return static
*/
public static function permissions(array $permissions)
{
static::$permissions = $permissions;
return new static;
}
/**
* Define the default permissions that should be available to new API tokens.
*
* @param array $permissions
* @return static
*/
public static function defaultApiTokenPermissions(array $permissions)
{
static::$defaultPermissions = $permissions;
return new static;
}
/**
* Return the permissions in the given list that are actually defined permissions for the application.
*
* @param array $permissions
* @return array
*/
public static function validPermissions(array $permissions)
{
return array_values(array_intersect($permissions, static::$permissions));
}
/**
* Determine if Jetstream is managing profile photos.
*
* @return bool
*/
public static function managesProfilePhotos()
{
return Features::managesProfilePhotos();
}
/**
* Determine if Jetstream is supporting API features.
*
* @return bool
*/
public static function hasApiFeatures()
{
return Features::hasApiFeatures();
}
/**
* Determine if Jetstream is supporting team features.
*
* @return bool
*/
public static function hasTeamFeatures()
{
return Features::hasTeamFeatures();
}
/**
* Determine if a given user model utilizes the "HasTeams" trait.
*
* @param \Illuminate\Database\Eloquent\Model
* @return bool
*/
public static function userHasTeamFeatures($user)
{
return (array_key_exists(HasTeams::class, class_uses_recursive($user)) ||
method_exists($user, 'currentTeam')) &&
static::hasTeamFeatures();
}
/**
* Determine if the application is using the terms confirmation feature.
*
* @return bool
*/
public static function hasTermsAndPrivacyPolicyFeature()
{
return Features::hasTermsAndPrivacyPolicyFeature();
}
/**
* Determine if the application is using any account deletion features.
*
* @return bool
*/
public static function hasAccountDeletionFeatures()
{
return Features::hasAccountDeletionFeatures();
}
/**
* Find a user instance by the given ID.
*
* @param int $id
* @return mixed
*/
public static function findUserByIdOrFail($id)
{
return static::newUserModel()->where('id', $id)->firstOrFail();
}
/**
* Find a user instance by the given email address or fail.
*
* @param string $email
* @return mixed
*/
public static function findUserByEmailOrFail(string $email)
{
return static::newUserModel()->where('email', $email)->firstOrFail();
}
/**
* Get the name of the user model used by the application.
*
* @return string
*/
public static function userModel()
{
return static::$userModel;
}
/**
* Get a new instance of the user model.
*
* @return mixed
*/
public static function newUserModel()
{
$model = static::userModel();
return new $model;
}
/**
* Specify the user model that should be used by Jetstream.
*
* @param string $model
* @return static
*/
public static function useUserModel(string $model)
{
static::$userModel = $model;
return new static;
}
/**
* Get the name of the team model used by the application.
*
* @return string
*/
public static function teamModel()
{
return static::$teamModel;
}
/**
* Get a new instance of the team model.
*
* @return mixed
*/
public static function newTeamModel()
{
$model = static::teamModel();
return new $model;
}
/**
* Specify the team model that should be used by Jetstream.
*
* @param string $model
* @return static
*/
public static function useTeamModel(string $model)
{
static::$teamModel = $model;
return new static;
}
/**
* Get the name of the membership model used by the application.
*
* @return string
*/
public static function membershipModel()
{
return static::$membershipModel;
}
/**
* Specify the membership model that should be used by Jetstream.
*
* @param string $model
* @return static
*/
public static function useMembershipModel(string $model)
{
static::$membershipModel = $model;
return new static;
}
/**
* Get the name of the team invitation model used by the application.
*
* @return string
*/
public static function teamInvitationModel()
{
return static::$teamInvitationModel;
}
/**
* Specify the team invitation model that should be used by Jetstream.
*
* @param string $model
* @return static
*/
public static function useTeamInvitationModel(string $model)
{
static::$teamInvitationModel = $model;
return new static;
}
/**
* Register a class / callback that should be used to create teams.
*
* @param string $class
* @return void
*/
public static function createTeamsUsing(string $class)
{
return app()->singleton(CreatesTeams::class, $class);
}
/**
* Register a class / callback that should be used to update team names.
*
* @param string $class
* @return void
*/
public static function updateTeamNamesUsing(string $class)
{
return app()->singleton(UpdatesTeamNames::class, $class);
}
/**
* Register a class / callback that should be used to add team members.
*
* @param string $class
* @return void
*/
public static function addTeamMembersUsing(string $class)
{
return app()->singleton(AddsTeamMembers::class, $class);
}
/**
* Register a class / callback that should be used to add team members.
*
* @param string $class
* @return void
*/
public static function inviteTeamMembersUsing(string $class)
{
return app()->singleton(InvitesTeamMembers::class, $class);
}
/**
* Register a class / callback that should be used to remove team members.
*
* @param string $class
* @return void
*/
public static function removeTeamMembersUsing(string $class)
{
return app()->singleton(RemovesTeamMembers::class, $class);
}
/**
* Register a class / callback that should be used to delete teams.
*
* @param string $class
* @return void
*/
public static function deleteTeamsUsing(string $class)
{
return app()->singleton(DeletesTeams::class, $class);
}
/**
* Register a class / callback that should be used to delete users.
*
* @param string $class
* @return void
*/
public static function deleteUsersUsing(string $class)
{
return app()->singleton(DeletesUsers::class, $class);
}
/**
* Manage Jetstream's Inertia settings.
*
* @return \Laravel\Jetstream\InertiaManager
*/
public static function inertia()
{
if (is_null(static::$inertiaManager)) {
static::$inertiaManager = new InertiaManager;
}
return static::$inertiaManager;
}
/**
* Find the path to a localized Markdown resource.
*
* @param string $name
* @return string|null
*/
public static function localizedMarkdownPath($name)
{
$localName = preg_replace('#(\.md)$#i', '.'.app()->getLocale().'$1', $name);
return Arr::first([
resource_path('markdown/'.$localName),
resource_path('markdown/'.$name),
], function ($path) {
return file_exists($path);
});
}
/**
* Configure Jetstream to not register its routes.
*
* @return static
*/
public static function ignoreRoutes()
{
static::$registersRoutes = false;
return new static;
}
}
@@ -0,0 +1,236 @@
<?php
namespace Laravel\Jetstream;
use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
use Inertia\Inertia;
use Laravel\Fortify\Events\PasswordUpdatedViaController;
use Laravel\Fortify\Fortify;
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
use Laravel\Jetstream\Http\Livewire\CreateTeamForm;
use Laravel\Jetstream\Http\Livewire\DeleteTeamForm;
use Laravel\Jetstream\Http\Livewire\DeleteUserForm;
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm;
use Laravel\Jetstream\Http\Livewire\NavigationMenu;
use Laravel\Jetstream\Http\Livewire\TeamMemberManager;
use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm;
use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm;
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
use Laravel\Jetstream\Http\Livewire\UpdateTeamNameForm;
use Laravel\Jetstream\Http\Middleware\ShareInertiaData;
use Livewire\Livewire;
class JetstreamServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/jetstream.php', 'jetstream');
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Fortify::viewPrefix('auth.');
$this->configurePublishing();
$this->configureRoutes();
$this->configureCommands();
RedirectResponse::macro('banner', function ($message): RedirectResponse {
/** @var \Illuminate\Http\RedirectResponse $this */
return $this->with('flash', [
'bannerStyle' => 'success',
'banner' => $message,
]);
});
RedirectResponse::macro('warningBanner', function ($message): RedirectResponse {
/** @var \Illuminate\Http\RedirectResponse $this */
return $this->with('flash', [
'bannerStyle' => 'warning',
'banner' => $message,
]);
});
RedirectResponse::macro('dangerBanner', function ($message): RedirectResponse {
/** @var \Illuminate\Http\RedirectResponse $this */
return $this->with('flash', [
'bannerStyle' => 'danger',
'banner' => $message,
]);
});
if (config('jetstream.stack') === 'inertia' && class_exists(Inertia::class)) {
$this->bootInertia();
}
if (config('jetstream.stack') === 'livewire' && class_exists(Livewire::class)) {
Livewire::component('navigation-menu', NavigationMenu::class);
Livewire::component('profile.update-profile-information-form', UpdateProfileInformationForm::class);
Livewire::component('profile.update-password-form', UpdatePasswordForm::class);
Livewire::component('profile.two-factor-authentication-form', TwoFactorAuthenticationForm::class);
Livewire::component('profile.logout-other-browser-sessions-form', LogoutOtherBrowserSessionsForm::class);
Livewire::component('profile.delete-user-form', DeleteUserForm::class);
if (Features::hasApiFeatures()) {
Livewire::component('api.api-token-manager', ApiTokenManager::class);
}
if (Features::hasTeamFeatures()) {
Livewire::component('teams.create-team-form', CreateTeamForm::class);
Livewire::component('teams.update-team-name-form', UpdateTeamNameForm::class);
Livewire::component('teams.team-member-manager', TeamMemberManager::class);
Livewire::component('teams.delete-team-form', DeleteTeamForm::class);
}
}
}
/**
* Configure publishing for the package.
*
* @return void
*/
protected function configurePublishing()
{
if (! $this->app->runningInConsole()) {
return;
}
$this->publishes([
__DIR__.'/../stubs/config/jetstream.php' => config_path('jetstream.php'),
], 'jetstream-config');
$this->publishes([
__DIR__.'/../database/migrations/0001_01_01_000000_create_users_table.php' => database_path('migrations/0001_01_01_000000_create_users_table.php'),
], 'jetstream-migrations');
$this->publishesMigrations([
__DIR__.'/../database/migrations/2020_05_21_100000_create_teams_table.php' => database_path('migrations/2020_05_21_100000_create_teams_table.php'),
__DIR__.'/../database/migrations/2020_05_21_200000_create_team_user_table.php' => database_path('migrations/2020_05_21_200000_create_team_user_table.php'),
__DIR__.'/../database/migrations/2020_05_21_300000_create_team_invitations_table.php' => database_path('migrations/2020_05_21_300000_create_team_invitations_table.php'),
], 'jetstream-team-migrations');
$this->publishes([
__DIR__.'/../routes/'.config('jetstream.stack').'.php' => base_path('routes/jetstream.php'),
], 'jetstream-routes');
$this->publishes([
__DIR__.'/../stubs/inertia/resources/js/Pages/Auth' => resource_path('js/Pages/Auth'),
__DIR__.'/../stubs/inertia/resources/js/Components/AuthenticationCard.vue' => resource_path('js/Components/AuthenticationCard.vue'),
__DIR__.'/../stubs/inertia/resources/js/Components/AuthenticationCardLogo.vue' => resource_path('js/Components/AuthenticationCardLogo.vue'),
__DIR__.'/../stubs/inertia/resources/js/Components/Checkbox.vue' => resource_path('js/Components/Checkbox.vue'),
], 'jetstream-inertia-auth-pages');
}
/**
* Configure the routes offered by the application.
*
* @return void
*/
protected function configureRoutes()
{
if (Jetstream::$registersRoutes) {
Route::group([
'namespace' => 'Laravel\Jetstream\Http\Controllers',
'domain' => config('jetstream.domain', null),
'prefix' => config('jetstream.prefix', config('jetstream.path')),
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/'.config('jetstream.stack').'.php');
});
}
}
/**
* Configure the commands offered by the application.
*
* @return void
*/
protected function configureCommands()
{
if (! $this->app->runningInConsole()) {
return;
}
$this->commands([
Console\InstallCommand::class,
]);
}
/**
* Boot any Inertia related services.
*
* @return void
*/
protected function bootInertia()
{
$kernel = $this->app->make(Kernel::class);
$kernel->appendMiddlewareToGroup('web', ShareInertiaData::class);
$kernel->appendToMiddlewarePriority(ShareInertiaData::class);
if (class_exists(HandleInertiaRequests::class)) {
$kernel->appendToMiddlewarePriority(HandleInertiaRequests::class);
}
Event::listen(function (PasswordUpdatedViaController $event) {
if (request()->hasSession()) {
request()->session()->put(['password_hash_sanctum' => Auth::user()->getAuthPassword()]);
}
});
Fortify::loginView(function () {
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
]);
});
Fortify::requestPasswordResetLinkView(function () {
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
});
Fortify::resetPasswordView(function (Request $request) {
return Inertia::render('Auth/ResetPassword', [
'email' => $request->input('email'),
'token' => $request->route('token'),
]);
});
Fortify::registerView(function () {
return Inertia::render('Auth/Register');
});
Fortify::verifyEmailView(function () {
return Inertia::render('Auth/VerifyEmail', [
'status' => session('status'),
]);
});
Fortify::twoFactorChallengeView(function () {
return Inertia::render('Auth/TwoFactorChallenge');
});
Fortify::confirmPasswordView(function () {
return Inertia::render('Auth/ConfirmPassword');
});
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Laravel\Jetstream\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\URL;
use Laravel\Jetstream\TeamInvitation as TeamInvitationModel;
class TeamInvitation extends Mailable
{
use Queueable, SerializesModels;
/**
* The team invitation instance.
*
* @var \Laravel\Jetstream\TeamInvitation
*/
public $invitation;
/**
* Create a new message instance.
*
* @param \Laravel\Jetstream\TeamInvitation $invitation
* @return void
*/
public function __construct(TeamInvitationModel $invitation)
{
$this->invitation = $invitation;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('emails.team-invitation', ['acceptUrl' => URL::signedRoute('team-invitations.accept', [
'invitation' => $this->invitation,
])])->subject(__('Team Invitation'));
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Relations\Pivot;
abstract class Membership extends Pivot
{
/**
* The table associated with the pivot model.
*
* @var string
*/
protected $table = 'team_user';
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Laravel\Jetstream;
class OwnerRole extends Role
{
/**
* Create a new role instance.
*
* @return void
*/
public function __construct()
{
parent::__construct('owner', 'Owner', ['*']);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Http\Response;
trait RedirectsActions
{
/**
* Get the redirect response for the given action.
*
* @param mixed $action
* @return \Illuminate\Http\Response
*/
public function redirectPath($action)
{
if (method_exists($action, 'redirectTo')) {
$response = $action->redirectTo();
} else {
$response = property_exists($action, 'redirectTo')
? $action->redirectTo
: config('fortify.home');
}
return $response instanceof Response ? $response : redirect($response);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Laravel\Jetstream;
use JsonSerializable;
class Role implements JsonSerializable
{
/**
* The key identifier for the role.
*
* @var string
*/
public $key;
/**
* The name of the role.
*
* @var string
*/
public $name;
/**
* The role's permissions.
*
* @var array
*/
public $permissions;
/**
* The role's description.
*
* @var string
*/
public $description;
/**
* Create a new role instance.
*
* @param string $key
* @param string $name
* @param array $permissions
* @return void
*/
public function __construct(string $key, string $name, array $permissions)
{
$this->key = $key;
$this->name = $name;
$this->permissions = $permissions;
}
/**
* Describe the role.
*
* @param string $description
* @return $this
*/
public function description(string $description)
{
$this->description = $description;
return $this;
}
/**
* Get the JSON serializable representation of the object.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return [
'key' => $this->key,
'name' => __($this->name),
'description' => __($this->description),
'permissions' => $this->permissions,
];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Laravel\Jetstream\Rules;
use Illuminate\Contracts\Validation\Rule;
use Laravel\Jetstream\Jetstream;
class Role implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return in_array($value, array_keys(Jetstream::$roles));
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('The :attribute must be a valid role.');
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Model;
abstract class Team extends Model
{
/**
* Get the owner of the team.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
return $this->belongsTo(Jetstream::userModel(), 'user_id');
}
/**
* Get all of the team's users including its owner.
*
* @return \Illuminate\Support\Collection
*/
public function allUsers()
{
return $this->users->merge([$this->owner]);
}
/**
* Get all of the users that belong to the team.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function users()
{
return $this->belongsToMany(Jetstream::userModel(), Jetstream::membershipModel())
->withPivot('role')
->withTimestamps()
->as('membership');
}
/**
* Determine if the given user belongs to the team.
*
* @param \App\Models\User $user
* @return bool
*/
public function hasUser($user)
{
return $this->users->contains($user) || $user->ownsTeam($this);
}
/**
* Determine if the given email address belongs to a user on the team.
*
* @param string $email
* @return bool
*/
public function hasUserWithEmail(string $email)
{
return $this->allUsers()->contains(function ($user) use ($email) {
return $user->email === $email;
});
}
/**
* Determine if the given user has the given permission on the team.
*
* @param \App\Models\User $user
* @param string $permission
* @return bool
*/
public function userHasPermission($user, $permission)
{
return $user->hasTeamPermission($this, $permission);
}
/**
* Get all of the pending user invitations for the team.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function teamInvitations()
{
return $this->hasMany(Jetstream::teamInvitationModel());
}
/**
* Remove the given user from the team.
*
* @param \App\Models\User $user
* @return void
*/
public function removeUser($user)
{
if ($user->current_team_id === $this->id) {
$user->forceFill([
'current_team_id' => null,
])->save();
}
$this->users()->detach($user);
}
/**
* Purge all of the team's resources.
*
* @return void
*/
public function purge()
{
$this->owner()->where('current_team_id', $this->id)
->update(['current_team_id' => null]);
$this->users()->where('current_team_id', $this->id)
->update(['current_team_id' => null]);
$this->users()->detach();
$this->delete();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Model;
class TeamInvitation extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'email',
'role',
];
/**
* Get the team that the invitation belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function team()
{
return $this->belongsTo(Jetstream::teamModel());
}
}