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
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
This starter kit is for Laravel 11.x and prior. For our latest starter kits, check out: [https://laravel.com/starter-kits](https://laravel.com/starter-kits).
+60
View File
@@ -0,0 +1,60 @@
{
"name": "laravel/jetstream",
"description": "Tailwind scaffolding for the Laravel framework.",
"keywords": ["laravel", "tailwind", "auth"],
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/jetstream/issues",
"source": "https://github.com/laravel/jetstream"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.2.0",
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"laravel/fortify": "^1.20",
"mobiledetect/mobiledetectlib": "^4.8.08",
"symfony/console": "^7.0"
},
"require-dev": {
"inertiajs/inertia-laravel": "^2.0",
"laravel/sanctum": "^4.0",
"livewire/livewire": "^3.3",
"mockery/mockery": "^1.0",
"orchestra/testbench": "^9.15|^10.8",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
"Laravel\\Jetstream\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Laravel\\Jetstream\\Tests\\": "tests/",
"App\\": "stubs/app/",
"Database\\Factories\\": "database/factories/"
}
},
"extra": {
"laravel": {
"providers": [
"Laravel\\Jetstream\\JetstreamServiceProvider"
]
}
},
"config": {
"audit": {
"block-insecure": false
},
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
+10
View File
@@ -0,0 +1,10 @@
<?php
use Laravel\Jetstream\Features;
return [
'stack' => 'inertia',
'middleware' => ['web'],
'features' => [Features::accountDeletion()],
'profile_photo_disk' => 'public',
];
@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Team>
*/
class TeamFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => $this->faker->unique()->company(),
'user_id' => User::factory(),
'personal_team' => true,
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace Database\Factories;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Jetstream\Features;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'remember_token' => Str::random(10),
'profile_photo_path' => null,
'current_team_id' => null,
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
/**
* Indicate that the user should have a personal team.
*/
public function withPersonalTeam(?callable $callback = null): static
{
if (! Features::hasTeamFeatures()) {
return $this->state([]);
}
return $this->has(
Team::factory()
->state(fn (array $attributes, User $user) => [
'name' => $user->name.'\'s Team',
'user_id' => $user->id,
'personal_team' => true,
])
->when(is_callable($callback), $callback),
'ownedTeams'
);
}
}
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index();
$table->string('name');
$table->boolean('personal_team');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('teams');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('team_user', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id');
$table->foreignId('user_id');
$table->string('role')->nullable();
$table->timestamps();
$table->unique(['team_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('team_user');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('team_invitations', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('email');
$table->string('role')->nullable();
$table->timestamps();
$table->unique(['team_id', 'email']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('team_invitations');
}
};
@@ -0,0 +1,23 @@
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->withPersonalTeam()->create();
User::factory()->withPersonalTeam()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
use Illuminate\Support\Facades\Route;
use Laravel\Jetstream\Http\Controllers\CurrentTeamController;
use Laravel\Jetstream\Http\Controllers\Inertia\ApiTokenController;
use Laravel\Jetstream\Http\Controllers\Inertia\CurrentUserController;
use Laravel\Jetstream\Http\Controllers\Inertia\OtherBrowserSessionsController;
use Laravel\Jetstream\Http\Controllers\Inertia\PrivacyPolicyController;
use Laravel\Jetstream\Http\Controllers\Inertia\ProfilePhotoController;
use Laravel\Jetstream\Http\Controllers\Inertia\TeamController;
use Laravel\Jetstream\Http\Controllers\Inertia\TeamMemberController;
use Laravel\Jetstream\Http\Controllers\Inertia\TermsOfServiceController;
use Laravel\Jetstream\Http\Controllers\Inertia\UserProfileController;
use Laravel\Jetstream\Http\Controllers\TeamInvitationController;
use Laravel\Jetstream\Jetstream;
Route::group(['middleware' => config('jetstream.middleware', ['web'])], function () {
if (Jetstream::hasTermsAndPrivacyPolicyFeature()) {
Route::get('/terms-of-service', [TermsOfServiceController::class, 'show'])->name('terms.show');
Route::get('/privacy-policy', [PrivacyPolicyController::class, 'show'])->name('policy.show');
}
$authMiddleware = config('jetstream.guard')
? 'auth:'.config('jetstream.guard')
: 'auth';
$authSessionMiddleware = config('jetstream.auth_session', false)
? config('jetstream.auth_session')
: null;
Route::group(['middleware' => array_values(array_filter([$authMiddleware, $authSessionMiddleware]))], function () {
// User & Profile...
Route::get('/user/profile', [UserProfileController::class, 'show'])
->name('profile.show');
Route::delete('/user/other-browser-sessions', [OtherBrowserSessionsController::class, 'destroy'])
->name('other-browser-sessions.destroy');
Route::delete('/user/profile-photo', [ProfilePhotoController::class, 'destroy'])
->name('current-user-photo.destroy');
if (Jetstream::hasAccountDeletionFeatures()) {
Route::delete('/user', [CurrentUserController::class, 'destroy'])
->name('current-user.destroy');
}
Route::group(['middleware' => 'verified'], function () {
// API...
if (Jetstream::hasApiFeatures()) {
Route::get('/user/api-tokens', [ApiTokenController::class, 'index'])->name('api-tokens.index');
Route::post('/user/api-tokens', [ApiTokenController::class, 'store'])->name('api-tokens.store');
Route::put('/user/api-tokens/{token}', [ApiTokenController::class, 'update'])->name('api-tokens.update');
Route::delete('/user/api-tokens/{token}', [ApiTokenController::class, 'destroy'])->name('api-tokens.destroy');
}
// Teams...
if (Jetstream::hasTeamFeatures()) {
Route::get('/teams/create', [TeamController::class, 'create'])->name('teams.create');
Route::post('/teams', [TeamController::class, 'store'])->name('teams.store');
Route::get('/teams/{team}', [TeamController::class, 'show'])->name('teams.show');
Route::put('/teams/{team}', [TeamController::class, 'update'])->name('teams.update');
Route::delete('/teams/{team}', [TeamController::class, 'destroy'])->name('teams.destroy');
Route::put('/current-team', [CurrentTeamController::class, 'update'])->name('current-team.update');
Route::post('/teams/{team}/members', [TeamMemberController::class, 'store'])->name('team-members.store');
Route::put('/teams/{team}/members/{user}', [TeamMemberController::class, 'update'])->name('team-members.update');
Route::delete('/teams/{team}/members/{user}', [TeamMemberController::class, 'destroy'])->name('team-members.destroy');
Route::get('/team-invitations/{invitation}', [TeamInvitationController::class, 'accept'])
->middleware(['signed'])
->name('team-invitations.accept');
Route::delete('/team-invitations/{invitation}', [TeamInvitationController::class, 'destroy'])
->name('team-invitations.destroy');
}
});
});
});
+49
View File
@@ -0,0 +1,49 @@
<?php
use Illuminate\Support\Facades\Route;
use Laravel\Jetstream\Http\Controllers\CurrentTeamController;
use Laravel\Jetstream\Http\Controllers\Livewire\ApiTokenController;
use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController;
use Laravel\Jetstream\Http\Controllers\Livewire\TeamController;
use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController;
use Laravel\Jetstream\Http\Controllers\Livewire\UserProfileController;
use Laravel\Jetstream\Http\Controllers\TeamInvitationController;
use Laravel\Jetstream\Jetstream;
Route::group(['middleware' => config('jetstream.middleware', ['web'])], function () {
if (Jetstream::hasTermsAndPrivacyPolicyFeature()) {
Route::get('/terms-of-service', [TermsOfServiceController::class, 'show'])->name('terms.show');
Route::get('/privacy-policy', [PrivacyPolicyController::class, 'show'])->name('policy.show');
}
$authMiddleware = config('jetstream.guard')
? 'auth:'.config('jetstream.guard')
: 'auth';
$authSessionMiddleware = config('jetstream.auth_session', false)
? config('jetstream.auth_session')
: null;
Route::group(['middleware' => array_values(array_filter([$authMiddleware, $authSessionMiddleware]))], function () {
// User & Profile...
Route::get('/user/profile', [UserProfileController::class, 'show'])->name('profile.show');
Route::group(['middleware' => 'verified'], function () {
// API...
if (Jetstream::hasApiFeatures()) {
Route::get('/user/api-tokens', [ApiTokenController::class, 'index'])->name('api-tokens.index');
}
// Teams...
if (Jetstream::hasTeamFeatures()) {
Route::get('/teams/create', [TeamController::class, 'create'])->name('teams.create');
Route::get('/teams/{team}', [TeamController::class, 'show'])->name('teams.show');
Route::put('/current-team', [CurrentTeamController::class, 'update'])->name('current-team.update');
Route::get('/team-invitations/{invitation}', [TeamInvitationController::class, 'accept'])
->middleware(['signed'])
->name('team-invitations.accept');
}
});
});
});
@@ -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());
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Validate and create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Actions\Fortify;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules;
/**
* Create a newly registered user.
*
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
])->validate();
return DB::transaction(function () use ($input) {
return tap(User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]), function (User $user) {
$this->createTeam($user);
});
});
}
/**
* Create a personal team for the user.
*/
protected function createTeam(User $user): void
{
$user->ownedTeams()->save(Team::forceCreate([
'user_id' => $user->id,
'name' => explode(' ', $user->name, 2)[0]."'s Team",
'personal_team' => true,
]));
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, mixed> $input
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
])->validateWithBag('updateProfileInformation');
if (isset($input['photo'])) {
$user->updateProfilePhoto($input['photo']);
}
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Closure;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Contracts\AddsTeamMembers;
use Laravel\Jetstream\Events\AddingTeamMember;
use Laravel\Jetstream\Events\TeamMemberAdded;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Rules\Role;
class AddTeamMember implements AddsTeamMembers
{
/**
* Add a new team member to the given team.
*/
public function add(User $user, Team $team, string $email, ?string $role = null): void
{
Gate::forUser($user)->authorize('addTeamMember', $team);
$this->validate($team, $email, $role);
$newTeamMember = Jetstream::findUserByEmailOrFail($email);
AddingTeamMember::dispatch($team, $newTeamMember);
$team->users()->attach(
$newTeamMember, ['role' => $role]
);
TeamMemberAdded::dispatch($team, $newTeamMember);
}
/**
* Validate the add member operation.
*/
protected function validate(Team $team, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules(), [
'email.exists' => __('We were unable to find a registered user with this email address.'),
])->after(
$this->ensureUserIsNotAlreadyOnTeam($team, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for adding a team member.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function rules(): array
{
return array_filter([
'email' => ['required', 'email', 'exists:users'],
'role' => Jetstream::hasRoles()
? ['required', 'string', new Role]
: null,
]);
}
/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure
{
return function ($validator) use ($team, $email) {
$validator->errors()->addIf(
$team->hasUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\Events\AddingTeam;
use Laravel\Jetstream\Jetstream;
class CreateTeam implements CreatesTeams
{
/**
* Validate and create a new team for the given user.
*
* @param array<string, string> $input
*/
public function create(User $user, array $input): Team
{
Gate::forUser($user)->authorize('create', Jetstream::newTeamModel());
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
])->validateWithBag('createTeam');
AddingTeam::dispatch($user);
$user->switchTeam($team = $user->ownedTeams()->create([
'name' => $input['name'],
'personal_team' => false,
]));
return $team;
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use Laravel\Jetstream\Contracts\DeletesTeams;
class DeleteTeam implements DeletesTeams
{
/**
* Delete the given team.
*/
public function delete(Team $team): void
{
$team->purge();
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\User;
use Laravel\Jetstream\Contracts\DeletesUsers;
class DeleteUser implements DeletesUsers
{
/**
* Delete the given user.
*/
public function delete(User $user): void
{
$user->deleteProfilePhoto();
$user->tokens->each->delete();
$user->delete();
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Laravel\Jetstream\Contracts\DeletesTeams;
use Laravel\Jetstream\Contracts\DeletesUsers;
class DeleteUser implements DeletesUsers
{
/**
* Create a new action instance.
*/
public function __construct(protected DeletesTeams $deletesTeams)
{
}
/**
* Delete the given user.
*/
public function delete(User $user): void
{
DB::transaction(function () use ($user) {
$this->deleteTeams($user);
$user->deleteProfilePhoto();
$user->tokens->each->delete();
$user->delete();
});
}
/**
* Delete the teams and team associations attached to the user.
*/
protected function deleteTeams(User $user): void
{
$user->teams()->detach();
$user->ownedTeams->each(function (Team $team) {
$this->deletesTeams->delete($team);
});
}
}
@@ -0,0 +1,88 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Closure;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Events\InvitingTeamMember;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Mail\TeamInvitation;
use Laravel\Jetstream\Rules\Role;
class InviteTeamMember implements InvitesTeamMembers
{
/**
* Invite a new team member to the given team.
*/
public function invite(User $user, Team $team, string $email, ?string $role = null): void
{
Gate::forUser($user)->authorize('addTeamMember', $team);
$this->validate($team, $email, $role);
InvitingTeamMember::dispatch($team, $email, $role);
$invitation = $team->teamInvitations()->create([
'email' => $email,
'role' => $role,
]);
Mail::to($email)->send(new TeamInvitation($invitation));
}
/**
* Validate the invite member operation.
*/
protected function validate(Team $team, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules($team), [
'email.unique' => __('This user has already been invited to the team.'),
])->after(
$this->ensureUserIsNotAlreadyOnTeam($team, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for inviting a team member.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function rules(Team $team): array
{
return array_filter([
'email' => [
'required', 'email',
Rule::unique(Jetstream::teamInvitationModel())->where(function (Builder $query) use ($team) {
$query->where('team_id', $team->id);
}),
],
'role' => Jetstream::hasRoles()
? ['required', 'string', new Role]
: null,
]);
}
/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure
{
return function ($validator) use ($team, $email) {
$validator->errors()->addIf(
$team->hasUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Events\TeamMemberRemoved;
class RemoveTeamMember implements RemovesTeamMembers
{
/**
* Remove the team member from the given team.
*/
public function remove(User $user, Team $team, User $teamMember): void
{
$this->authorize($user, $team, $teamMember);
$this->ensureUserDoesNotOwnTeam($teamMember, $team);
$team->removeUser($teamMember);
TeamMemberRemoved::dispatch($team, $teamMember);
}
/**
* Authorize that the user can remove the team member.
*/
protected function authorize(User $user, Team $team, User $teamMember): void
{
if (! Gate::forUser($user)->check('removeTeamMember', $team) &&
$user->id !== $teamMember->id) {
throw new AuthorizationException;
}
}
/**
* Ensure that the currently authenticated user does not own the team.
*/
protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void
{
if ($teamMember->id === $team->owner->id) {
throw ValidationException::withMessages([
'team' => [__('You may not leave a team that you created.')],
])->errorBag('removeTeamMember');
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator;
use Laravel\Jetstream\Contracts\UpdatesTeamNames;
class UpdateTeamName implements UpdatesTeamNames
{
/**
* Validate and update the given team's name.
*
* @param array<string, string> $input
*/
public function update(User $user, Team $team, array $input): void
{
Gate::forUser($user)->authorize('update', $team);
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
])->validateWithBag('updateTeamName');
$team->forceFill([
'name' => $input['name'],
])->save();
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Laravel\Jetstream\Membership as JetstreamMembership;
class Membership extends JetstreamMembership
{
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Jetstream\Events\TeamCreated;
use Laravel\Jetstream\Events\TeamDeleted;
use Laravel\Jetstream\Events\TeamUpdated;
use Laravel\Jetstream\Team as JetstreamTeam;
class Team extends JetstreamTeam
{
/** @use HasFactory<\Database\Factories\TeamFactory> */
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'personal_team',
];
/**
* The event map for the model.
*
* @var array<string, class-string>
*/
protected $dispatchesEvents = [
'created' => TeamCreated::class,
'updated' => TeamUpdated::class,
'deleted' => TeamDeleted::class,
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'personal_team' => 'boolean',
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\TeamInvitation as JetstreamTeamInvitation;
class TeamInvitation extends JetstreamTeamInvitation
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
'role',
];
/**
* Get the team that the invitation belongs to.
*/
public function team(): BelongsTo
{
return $this->belongsTo(Jetstream::teamModel());
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory;
use HasProfilePhoto;
use Notifiable;
use TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
];
/**
* The accessors to append to the model's array form.
*
* @var array<int, string>
*/
protected $appends = [
'profile_photo_url',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Jetstream\HasTeams;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory;
use HasProfilePhoto;
use HasTeams;
use Notifiable;
use TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
];
/**
* The accessors to append to the model's array form.
*
* @var array<int, string>
*/
protected $appends = [
'profile_photo_url',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Policies;
use App\Models\Team;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class TeamPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Team $team): bool
{
return $user->belongsToTeam($team);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Team $team): bool
{
return $user->ownsTeam($team);
}
/**
* Determine whether the user can add team members.
*/
public function addTeamMember(User $user, Team $team): bool
{
return $user->ownsTeam($team);
}
/**
* Determine whether the user can update team member permissions.
*/
public function updateTeamMember(User $user, Team $team): bool
{
return $user->ownsTeam($team);
}
/**
* Determine whether the user can remove team members.
*/
public function removeTeamMember(User $user, Team $team): bool
{
return $user->ownsTeam($team);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Team $team): bool
{
return $user->ownsTeam($team);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Providers;
use App\Actions\Jetstream\DeleteUser;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Laravel\Jetstream\Jetstream;
class JetstreamServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->configurePermissions();
Jetstream::deleteUsersUsing(DeleteUser::class);
Vite::prefetch(concurrency: 3);
}
/**
* Configure the permissions that are available within the application.
*/
protected function configurePermissions(): void
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::permissions([
'create',
'read',
'update',
'delete',
]);
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Providers;
use App\Actions\Jetstream\AddTeamMember;
use App\Actions\Jetstream\CreateTeam;
use App\Actions\Jetstream\DeleteTeam;
use App\Actions\Jetstream\DeleteUser;
use App\Actions\Jetstream\InviteTeamMember;
use App\Actions\Jetstream\RemoveTeamMember;
use App\Actions\Jetstream\UpdateTeamName;
use Illuminate\Support\ServiceProvider;
use Laravel\Jetstream\Jetstream;
class JetstreamServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->configurePermissions();
Jetstream::createTeamsUsing(CreateTeam::class);
Jetstream::updateTeamNamesUsing(UpdateTeamName::class);
Jetstream::addTeamMembersUsing(AddTeamMember::class);
Jetstream::inviteTeamMembersUsing(InviteTeamMember::class);
Jetstream::removeTeamMembersUsing(RemoveTeamMember::class);
Jetstream::deleteTeamsUsing(DeleteTeam::class);
Jetstream::deleteUsersUsing(DeleteUser::class);
}
/**
* Configure the roles and permissions that are available within the application.
*/
protected function configurePermissions(): void
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::role('admin', 'Administrator', [
'create',
'read',
'update',
'delete',
])->description('Administrator users can perform any action.');
Jetstream::role('editor', 'Editor', [
'read',
'create',
'update',
])->description('Editor users have the ability to read, create, and update.');
}
}

Some files were not shown because too many files have changed in this diff Show More