diff --git a/composer.json b/composer.json index 7f977f26..b6a707a2 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,6 @@ "orchestra/testbench": "^9.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.2", - "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.5" }, "autoload": { diff --git a/config/filament.php b/config/filament.php index 2f718127..4f814b52 100644 --- a/config/filament.php +++ b/config/filament.php @@ -8,5 +8,6 @@ 'providers' => [ // Providers::github(), ], - 'component' => 'socialstream::components.socialstream', + 'components' => 'socialstream::components.socialstream', + 'filament-route' => 'filament.admin.pages.dashboard', ]; diff --git a/database/migrations/0001_01_01_000000_create_breeze_users_table.php b/database/migrations/0001_01_01_000000_create_breeze_users_table.php deleted file mode 100644 index 601b4243..00000000 --- a/database/migrations/0001_01_01_000000_create_breeze_users_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password')->nullable(); - $table->rememberToken(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - } -}; diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php deleted file mode 100644 index 06a0dafd..00000000 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ /dev/null @@ -1,51 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password')->nullable(); - $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'); - } -}; diff --git a/database/migrations/0001_01_01_000001_make_password_nullable_on_users_table.php b/database/migrations/0001_01_01_000000_make_password_nullable_on_users_table.php similarity index 100% rename from database/migrations/0001_01_01_000001_make_password_nullable_on_users_table.php rename to database/migrations/0001_01_01_000000_make_password_nullable_on_users_table.php diff --git a/database/migrations/0001_01_01_000002_create_connected_accounts_table.php b/database/migrations/0001_01_01_000001_create_connected_accounts_table.php similarity index 100% rename from database/migrations/0001_01_01_000002_create_connected_accounts_table.php rename to database/migrations/0001_01_01_000001_create_connected_accounts_table.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 649776a4..00000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,11 +0,0 @@ -parameters: - paths: - - config - - database - - routes - - src - - level: 0 - - ignoreErrors: - - "#Unsafe usage of new static\\(\\)#" diff --git a/src/Actions/AuthenticateOAuthCallback.php b/src/Actions/AuthenticateOAuthCallback.php index 2ac97206..7770824e 100644 --- a/src/Actions/AuthenticateOAuthCallback.php +++ b/src/Actions/AuthenticateOAuthCallback.php @@ -3,8 +3,10 @@ namespace JoelButcher\Socialstream\Actions; use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Contracts\Auth\Guard; +use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Routing\Pipeline; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; use Illuminate\Support\MessageBag; @@ -29,6 +31,11 @@ use JoelButcher\Socialstream\Features; use JoelButcher\Socialstream\Providers; use JoelButcher\Socialstream\Socialstream; +use Laravel\Fortify\Actions\CanonicalizeUsername; +use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled; +use Laravel\Fortify\Actions\PrepareAuthenticatedSession; +use Laravel\Fortify\Features as FortifyFeatures; +use Laravel\Fortify\Fortify; use Laravel\Jetstream\Jetstream; use Laravel\Socialite\Contracts\User as ProviderUser; @@ -41,7 +48,7 @@ class AuthenticateOAuthCallback implements AuthenticatesOAuthCallback * Create a new controller instance. */ public function __construct( - protected Guard $guard, + protected StatefulGuard $guard, protected CreatesUserFromProvider $createsUser, protected CreatesConnectedAccounts $createsConnectedAccounts, protected UpdatesConnectedAccounts $updatesConnectedAccounts @@ -108,29 +115,74 @@ public function authenticate(string $provider, ProviderUser $providerAccount): S /** * Handle the registration of a new user. */ - protected function register(string $provider, ProviderUser $providerAccount): SocialstreamResponse + protected function register(string $provider, ProviderUser $providerAccount): SocialstreamResponse|RedirectResponse { $user = $this->createsUser->create($provider, $providerAccount); - $this->guard->login($user, Socialstream::hasRememberSessionFeatures()); + return tap( + (new Pipeline(app()))->send(request())->through(array_filter([ + function ($request, $next) use ($user) { + $this->guard->login($user, Socialstream::hasRememberSessionFeatures()); - event(new NewOAuthRegistration($user, $provider, $providerAccount)); - - return app(OAuthRegisterResponse::class); + return $next($request); + }, + ]))->then(fn () => app(OAuthRegisterResponse::class)), + fn () => event(new NewOAuthRegistration($user, $provider, $providerAccount)) + ); } /** * Authenticate the given user and return a login response. */ - protected function login(Authenticatable $user, mixed $account, string $provider, ProviderUser $providerAccount): SocialstreamResponse + protected function login(Authenticatable $user, mixed $account, string $provider, ProviderUser $providerAccount): SocialstreamResponse|RedirectResponse { $this->updatesConnectedAccounts->update($user, $account, $provider, $providerAccount); - $this->guard->login($user, Socialstream::hasRememberSessionFeatures()); + return tap( + $this->loginPipeline(request(), $user)->then(fn () => app(OAuthLoginResponse::class)), + fn () => event(new OAuthLogin($user, $provider, $account, $providerAccount)), + ); + } + + protected function loginPipeline(Request $request, Authenticatable $user): Pipeline + { + if (! class_exists(Fortify::class)) { + return (new Pipeline(app()))->send($request)->through(array_filter([ + function ($request, $next) use ($user) { + $this->guard->login($user, Socialstream::hasRememberSessionFeatures()); + + if ($request->hasSession()) { + $request->session()->regenerate(); + } + + return $next($request); + }, + ])); + } - event(new OAuthLogin($user, $provider, $account, $providerAccount)); + if (Fortify::$authenticateThroughCallback) { + return (new Pipeline(app()))->send($request)->through(array_filter( + call_user_func(Fortify::$authenticateThroughCallback, $request) + )); + } + + if (is_array(config('fortify.pipelines.login'))) { + return (new Pipeline(app()))->send($request)->through(array_filter( + config('fortify.pipelines.login') + )); + } - return app(OAuthLoginResponse::class); + return (new Pipeline(app()))->send($request)->through(array_filter([ + config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class, + config('fortify.lowercase_usernames') ? CanonicalizeUsername::class : null, + FortifyFeatures::enabled(FortifyFeatures::twoFactorAuthentication()) ? RedirectIfTwoFactorAuthenticatable::class : null, + function ($request, $next) use ($user) { + $this->guard->login($user, Socialstream::hasRememberSessionFeatures()); + + return $next($request); + }, + PrepareAuthenticatedSession::class, + ])); } /** @@ -205,7 +257,7 @@ private function flashError(string $error): void */ private function canRegister(mixed $user, mixed $account): bool { - if (! is_null($user) || !is_null($account)) { + if (! is_null($user) || ! is_null($account)) { return false; } diff --git a/src/Actions/CreateUserWithTeamsFromProvider.php b/src/Actions/CreateUserWithTeamsFromProvider.php index 0d03bc50..55e27413 100644 --- a/src/Actions/CreateUserWithTeamsFromProvider.php +++ b/src/Actions/CreateUserWithTeamsFromProvider.php @@ -3,7 +3,7 @@ namespace JoelButcher\Socialstream\Actions; use App\Models\Team; -use app\Models\User; +use App\Models\User; use Illuminate\Support\Facades\DB; use JoelButcher\Socialstream\Contracts\CreatesConnectedAccounts; use JoelButcher\Socialstream\Contracts\CreatesUserFromProvider; diff --git a/src/Actions/RedirectIfTwoFactorAuthenticatable.php b/src/Actions/RedirectIfTwoFactorAuthenticatable.php new file mode 100644 index 00000000..24d285ad --- /dev/null +++ b/src/Actions/RedirectIfTwoFactorAuthenticatable.php @@ -0,0 +1,37 @@ +fireFailedEvent($request); + + $this->throwFailedAuthenticationException($request); + } + }); + } + + $socialUser = app(ResolvesSocialiteUsers::class) + ->resolve($request->route('provider')); + + return tap(Socialstream::$userModel::where('email', $socialUser->getEmail())->first(), function ($user) use ($request, $socialUser) { + if (! $user || ! Socialstream::$connectedAccountModel::where('email', $socialUser->getEmail())->first()) { + $this->fireFailedEvent($request, $user); + + $this->throwFailedAuthenticationException($request); + } + }); + } +} diff --git a/src/Concerns/InteractsWithComposer.php b/src/Concerns/InteractsWithComposer.php index 0e3ba13f..3d86f981 100644 --- a/src/Concerns/InteractsWithComposer.php +++ b/src/Concerns/InteractsWithComposer.php @@ -3,6 +3,7 @@ namespace JoelButcher\Socialstream\Concerns; use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; trait InteractsWithComposer @@ -82,4 +83,12 @@ protected function buildBaseComposerCommand(string $command, array $packages, st $packages ); } + + /** + * Get the path to the appropriate PHP binary. + */ + protected function phpBinary(): string + { + return (new PhpExecutableFinder())->find(false) ?: 'php'; + } } diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 65833d21..0086edab 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Contracts\Console\PromptsForMissingInput; +use Illuminate\Support\ServiceProvider; use JoelButcher\Socialstream\Concerns\InteractsWithComposer; use JoelButcher\Socialstream\Concerns\InteractsWithNode; use JoelButcher\Socialstream\Installer\Enums\BreezeInstallStack; @@ -12,9 +13,11 @@ use JoelButcher\Socialstream\Installer\Enums\JetstreamInstallStack; use JoelButcher\Socialstream\Installer\InstallManager; use Laravel\Fortify\Features as FortifyFeatures; +use Laravel\Fortify\FortifyServiceProvider; use Laravel\Jetstream\Jetstream; use Pest\TestSuite; 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; @@ -26,6 +29,7 @@ use function Laravel\Prompts\select; use function Laravel\Prompts\warning; +#[AsCommand(name: 'socialstream:install')] class InstallCommand extends Command implements PromptsForMissingInput { use InteractsWithComposer; @@ -84,6 +88,18 @@ public function handle(InstallManager $installManager): ?int return self::SUCCESS; } + /** + * Register the Fortify service provider in the application configuration file. + */ + protected function registerFortifyServiceProvider(): void + { + if (! method_exists(ServiceProvider::class, 'addProviderToBootstrapFile')) { + return; + } + + ServiceProvider::addProviderToBootstrapFile(\App\Providers\FortifyServiceProvider::class); + } + /** * Prompt for missing input arguments using the returned questions. */ @@ -168,7 +184,7 @@ protected function promptForMissingArgumentsUsing(): array protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void { if ($this->isUsingFilament()) { - $input->setOption('pest', select( + $input->setOption('pest', $this->option('pest') || select( label: 'Which testing framework do you prefer?', options: ['PHPUnit', 'Pest'], default: $this->isUsingPest() ? 'Pest' : 'PHPUnit' @@ -218,17 +234,17 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp ] ))->each(fn ($option) => $input->setOption($option, true)); } else { - $input->setOption('dark', confirm( - label: 'Would you like dark mode support?', - default: false - )); + $input->setOption('dark', $this->option('dark') || confirm( + label: 'Would you like dark mode support?', + default: false + )); } - $input->setOption('pest', select( - label: 'Which testing framework do you prefer?', - options: ['PHPUnit', 'Pest'], - default: $this->isUsingPest() ? 'pest' : 'phpunit' - ) === 'Pest'); + $input->setOption('pest', $this->option('pest') || select( + label: 'Which testing framework do you prefer?', + options: ['PHPUnit', 'Pest'], + default: $this->isUsingPest() ? 'pest' : 'phpunit' + ) === 'Pest'); } } diff --git a/src/Filament/SocialstreamPlugin.php b/src/Filament/SocialstreamPlugin.php index 58a2fcc8..4e6b7f15 100644 --- a/src/Filament/SocialstreamPlugin.php +++ b/src/Filament/SocialstreamPlugin.php @@ -28,6 +28,15 @@ public function register(Panel $panel): void 'errors' => session('errors') ?? new ViewErrorBag(), ]) : ''; }); + + if ($panel->hasRegistration()) { + $panel->renderHook('panels::auth.register.form.after', function () { + return Socialstream::show() ? + view(config('socialstream.component', 'socialstream::components.socialstream'), [ + 'errors' => session('errors') ?? new ViewErrorBag(), + ]) : ''; + }); + } } public function boot(Panel $panel): void diff --git a/src/Http/Responses/OAuthLoginResponse.php b/src/Http/Responses/OAuthLoginResponse.php index 71310e10..1bf2e7cc 100644 --- a/src/Http/Responses/OAuthLoginResponse.php +++ b/src/Http/Responses/OAuthLoginResponse.php @@ -2,38 +2,44 @@ namespace JoelButcher\Socialstream\Http\Responses; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Session; +use Illuminate\Http\Request; use JoelButcher\Socialstream\Concerns\ConfirmsFilament; use JoelButcher\Socialstream\Concerns\InteractsWithComposer; use JoelButcher\Socialstream\Contracts\OAuthLoginResponse as LoginResponseContract; use JoelButcher\Socialstream\Socialstream; use Laravel\Fortify\Contracts\LoginResponse as FortifyLoginResponse; +use Laravel\Fortify\Fortify; class OAuthLoginResponse implements LoginResponseContract { use ConfirmsFilament; use InteractsWithComposer; - public function toResponse($request): RedirectResponse + public function toResponse($request): RedirectResponse|FortifyLoginResponse { - return Socialstream::redirects('login') - ? redirect()->intended(Socialstream::redirects('login')) - : $this->defaultResponse(); + return match (true) { + $this->usesFilament() && $this->hasFilamentAuthRoutes() => redirect()->route( + config('socialstream.filament-route', 'filament.admin.pages.dashboard') + ), + $this->hasComposerPackage('laravel/jetstream') => $this->fortifyResponse($request), + $this->hasComposerPackage('laravel/breeze') => redirect()->route('dashboard'), + default => $this->defaultResponse(), + }; } - private function defaultResponse(): RedirectResponse|FortifyLoginResponse + private function fortifyResponse(Request $request): JsonResponse|RedirectResponse { - return match (true) { - $this->usesFilament() && $this->hasFilamentAuthRoutes() => redirect()->route('filament.home'), - $this->hasComposerPackage('laravel/breeze') => redirect() - ->route('dashboard'), - $this->hasComposerPackage('laravel/jetstream') => app(FortifyLoginResponse::class), - default => redirect() - ->to(route('dashboard', absolute: false)), - }; + return $request->wantsJson() + ? response()->json(['two_factor' => false]) + : redirect()->intended(Fortify::redirects('login')); } + private function defaultResponse(): RedirectResponse + { + return Socialstream::redirects('login') + ? redirect()->intended(Socialstream::redirects('login')) + : redirect()->to(route('dashboard', absolute: false)); + } } diff --git a/src/Http/Responses/OAuthRegisterResponse.php b/src/Http/Responses/OAuthRegisterResponse.php index f9437d75..61a83ddf 100644 --- a/src/Http/Responses/OAuthRegisterResponse.php +++ b/src/Http/Responses/OAuthRegisterResponse.php @@ -2,37 +2,45 @@ namespace JoelButcher\Socialstream\Http\Responses; -use App\Providers\RouteServiceProvider; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Session; +use Illuminate\Http\Request; use JoelButcher\Socialstream\Concerns\ConfirmsFilament; use JoelButcher\Socialstream\Concerns\InteractsWithComposer; use JoelButcher\Socialstream\Contracts\OAuthRegisterResponse as RegisterResponseContract; use JoelButcher\Socialstream\Socialstream; use Laravel\Fortify\Contracts\RegisterResponse as FortifyRegisterResponse; +use Laravel\Fortify\Fortify; +use Laravel\Fortify\Http\Responses\RegisterResponse; class OAuthRegisterResponse implements RegisterResponseContract { use ConfirmsFilament; use InteractsWithComposer; - public function toResponse($request): RedirectResponse + public function toResponse($request): RedirectResponse|RegisterResponse { - return Socialstream::redirects('register') - ? redirect()->intended(Socialstream::redirects('register')) - : $this->defaultResponse(); + return match (true) { + $this->usesFilament() && $this->hasFilamentAuthRoutes() => redirect()->route( + config('socialstream.filament-route', 'filament.admin.pages.dashboard') + ), + $this->hasComposerPackage('laravel/jetstream') => $this->fortifyResponse($request), + $this->hasComposerPackage('laravel/breeze') => redirect()->route('dashboard'), + default => $this->defaultResponse(), + }; } - private function defaultResponse(): RedirectResponse|FortifyRegisterResponse + private function fortifyResponse(Request $request): JsonResponse|RedirectResponse { - return match (true) { - $this->usesFilament() && $this->hasFilamentAuthRoutes() => redirect()->route('filament.home'), - $this->hasComposerPackage('laravel/breeze') => redirect() - ->route('dashboard'), - $this->hasComposerPackage('laravel/jetstream') => app(FortifyRegisterResponse::class), - default => redirect() - ->to(route('dashboard', absolute: false)), - }; + return $request->wantsJson() + ? new JsonResponse('', 201) + : redirect()->intended(Fortify::redirects('register')); + } + + private function defaultResponse(): RedirectResponse + { + return Socialstream::redirects('register') + ? redirect()->intended(Socialstream::redirects('register')) + : redirect()->to(route('dashboard', absolute: false)); } } diff --git a/src/Installer/Drivers/Breeze/BladeDriver.php b/src/Installer/Drivers/Breeze/BladeDriver.php index ad89df6d..dac3dd60 100644 --- a/src/Installer/Drivers/Breeze/BladeDriver.php +++ b/src/Installer/Drivers/Breeze/BladeDriver.php @@ -8,9 +8,6 @@ class BladeDriver extends BreezeDriver { - /** - * Specify the stack used by this installer. - */ protected static function stack(): BreezeInstallStack { return BreezeInstallStack::Blade; @@ -25,9 +22,6 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/default/resources/views/auth/login.blade.php', resource_path('views/auth/login.blade.php')); @@ -36,9 +30,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/default/resources/views/profile/edit.blade.php', resource_path('views/profile/edit.blade.php')); @@ -48,9 +39,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { (new Filesystem)->copyDirectory(__DIR__.'/../../../../stubs/breeze/default/resources/views/components/socialstream-icons', resource_path('views/components/socialstream-icons')); diff --git a/src/Installer/Drivers/Breeze/BreezeDriver.php b/src/Installer/Drivers/Breeze/BreezeDriver.php index fb838f1c..69c3f088 100644 --- a/src/Installer/Drivers/Breeze/BreezeDriver.php +++ b/src/Installer/Drivers/Breeze/BreezeDriver.php @@ -16,9 +16,6 @@ abstract class BreezeDriver extends Driver { - /** - * Specify the stack used by this installer. - */ abstract protected static function stack(): BreezeInstallStack; protected function ensureDependenciesAreInstalled(string $composerBinary, InstallOptions ...$options): void @@ -59,19 +56,8 @@ class_exists('App\Http\Middleware\HandleInertiaRequests') \Laravel\Prompts\info('Laravel Breeze has been installed successfully!'); } - /** - * Copy the Socialstream routes. - */ protected function installRoutes(): static { - $folder = Str::of(match(static::stack()) { - BreezeInstallStack::Blade, - BreezeInstallStack::Livewire, - BreezeInstallStack::FunctionalLivewire => 'livewire', - BreezeInstallStack::Vue, - BreezeInstallStack::React, => 'inertia', - })->lower()->toString(); - copy(__DIR__.'/../../../../stubs/breeze/default/routes/socialstream.php', base_path('routes/socialstream.php')); File::append(base_path('routes/web.php'), data: "require __DIR__.'/socialstream.php';"); @@ -79,9 +65,6 @@ protected function installRoutes(): static return $this; } - /** - * Copy all the app files required for the stack. - */ protected function copyAppFiles(): static { copy(__DIR__.'/../../../../stubs/breeze/default/app/Http/Controllers/Auth/ConnectedAccountController.php', app_path('Http/Controllers/Auth/ConnectedAccountController.php')); @@ -91,9 +74,6 @@ protected function copyAppFiles(): static return $this; } - /** - * Copy the Socialstream models and their factories to the base "app" directory. - */ protected function copyModelsAndFactories(): static { parent::copyModelsAndFactories(); @@ -104,9 +84,6 @@ protected function copyModelsAndFactories(): static return $this; } - /** - * Copy the Socialstream test files to the apps "tests" directory for the given test runner. - */ protected function copyTests(TestRunner $testRunner): static { copy(from: match ($testRunner) { diff --git a/src/Installer/Drivers/Breeze/FunctionalLivewireDriver.php b/src/Installer/Drivers/Breeze/FunctionalLivewireDriver.php index 291de5c4..2e4e0308 100644 --- a/src/Installer/Drivers/Breeze/FunctionalLivewireDriver.php +++ b/src/Installer/Drivers/Breeze/FunctionalLivewireDriver.php @@ -8,9 +8,6 @@ class FunctionalLivewireDriver extends BreezeDriver { - /** - * Specify the stack used by this installer. - */ protected static function stack(): BreezeInstallStack { return BreezeInstallStack::FunctionalLivewire; @@ -24,17 +21,11 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy all the app files required for the stack. - */ protected function copyAppFiles(): static { return $this; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/livewire-functional/resources/views/livewire/pages/auth/login.blade.php', resource_path('views/livewire/pages/auth/login.blade.php')); @@ -43,9 +34,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/livewire/resources/views/profile.blade.php', resource_path('views/profile.blade.php')); @@ -56,9 +44,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { (new Filesystem)->copyDirectory(__DIR__.'/../../../../stubs/breeze/default/resources/views/components/socialstream-icons', resource_path('views/components/socialstream-icons')); diff --git a/src/Installer/Drivers/Breeze/LivewireDriver.php b/src/Installer/Drivers/Breeze/LivewireDriver.php index f336c922..fd5abaf4 100644 --- a/src/Installer/Drivers/Breeze/LivewireDriver.php +++ b/src/Installer/Drivers/Breeze/LivewireDriver.php @@ -8,9 +8,6 @@ class LivewireDriver extends BreezeDriver { - /** - * Specify the stack used by this installer. - */ protected static function stack(): BreezeInstallStack { return BreezeInstallStack::Livewire; @@ -24,17 +21,11 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy all the app files required for the stack. - */ protected function copyAppFiles(): static { return $this; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/livewire/resources/views/livewire/pages/auth/login.blade.php', resource_path('views/livewire/pages/auth/login.blade.php')); @@ -43,9 +34,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/breeze/livewire/resources/views/profile.blade.php', resource_path('views/profile.blade.php')); @@ -56,9 +44,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { (new Filesystem)->copyDirectory(__DIR__.'/../../../../stubs/breeze/default/resources/views/components/socialstream-icons', resource_path('views/components/socialstream-icons')); diff --git a/src/Installer/Drivers/Breeze/ReactInertiaDriver.php b/src/Installer/Drivers/Breeze/ReactInertiaDriver.php index 73704fa6..47ced6dc 100644 --- a/src/Installer/Drivers/Breeze/ReactInertiaDriver.php +++ b/src/Installer/Drivers/Breeze/ReactInertiaDriver.php @@ -25,9 +25,6 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy all the app files required for the stack. - */ protected function copyAppFiles(): static { copy(__DIR__.'/../../../../stubs/breeze/default/app/Http/Controllers/Auth/ConnectedAccountController.php', app_path('Http/Controllers/Auth/ConnectedAccountController.php')); @@ -37,9 +34,6 @@ protected function copyAppFiles(): static return $this; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { @@ -55,9 +49,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { @@ -75,9 +66,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { diff --git a/src/Installer/Drivers/Breeze/VueInertiaDriver.php b/src/Installer/Drivers/Breeze/VueInertiaDriver.php index d2727b93..365fb285 100644 --- a/src/Installer/Drivers/Breeze/VueInertiaDriver.php +++ b/src/Installer/Drivers/Breeze/VueInertiaDriver.php @@ -25,9 +25,6 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy all the app files required for the stack. - */ protected function copyAppFiles(): static { copy(__DIR__.'/../../../../stubs/breeze/default/app/Http/Controllers/Auth/ConnectedAccountController.php', app_path('Http/Controllers/Auth/ConnectedAccountController.php')); @@ -37,9 +34,6 @@ protected function copyAppFiles(): static return $this; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { @@ -55,9 +49,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { @@ -75,9 +66,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { if (in_array(InstallOptions::TypeScript, $options)) { diff --git a/src/Installer/Drivers/Driver.php b/src/Installer/Drivers/Driver.php index a06bb172..84d5c2e8 100644 --- a/src/Installer/Drivers/Driver.php +++ b/src/Installer/Drivers/Driver.php @@ -11,7 +11,6 @@ use JoelButcher\Socialstream\Installer\Enums\TestRunner; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Finder\Finder; -use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use function Laravel\Prompts\spin; @@ -43,6 +42,9 @@ protected function postInstall(string $composerBinary, InstallOptions ...$option // } + /** + * Install the stack with the given options + */ public function install(string $composerBinary = 'global', InstallOptions ...$options): void { $this->ensureDependenciesAreInstalled($composerBinary, ...$options); @@ -83,7 +85,7 @@ private function publishFiles(): static spin(callback: function () { $outputStyle = new BufferedOutput; - (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-config', '--force'], base_path())) + (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-config'], base_path())) ->setTimeout(null) ->run(function ($type, $output) use ($outputStyle) { $outputStyle->write($output); @@ -95,13 +97,13 @@ private function publishFiles(): static $outputStyle->write($output); }); - (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-routes', '--force'], base_path())) + (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-routes'], base_path())) ->setTimeout(null) ->run(function ($type, $output) use ($outputStyle) { $outputStyle->write($output); }); - (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-actions', '--force'], base_path())) + (new Process([$this->phpBinary(), 'artisan', 'vendor:publish', '--tag=socialstream-actions'], base_path())) ->setTimeout(null) ->run(function ($type, $output) use ($outputStyle) { $outputStyle->write($output); @@ -281,11 +283,9 @@ protected function runCommands(array $commands, array $env = []): Process return $process; } - /** - * Get the path to the appropriate PHP binary. - */ - protected function phpBinary(): string + /** Replace a given string within a given file. */ + protected function replaceInFile($search, $replace, $path) { - return (new PhpExecutableFinder())->find(false) ?: 'php'; + file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); } } diff --git a/src/Installer/Drivers/Filament/FilamentDriver.php b/src/Installer/Drivers/Filament/FilamentDriver.php index 8ec42ec3..7c1800cb 100644 --- a/src/Installer/Drivers/Filament/FilamentDriver.php +++ b/src/Installer/Drivers/Filament/FilamentDriver.php @@ -2,10 +2,12 @@ namespace JoelButcher\Socialstream\Installer\Drivers\Filament; +use Illuminate\Support\ServiceProvider; use JoelButcher\Socialstream\Installer\Drivers\Driver; use JoelButcher\Socialstream\Installer\Enums\InstallOptions; use JoelButcher\Socialstream\Installer\Enums\TestRunner; use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; use function Laravel\Prompts\spin; @@ -13,55 +15,49 @@ class FilamentDriver extends Driver { - protected function postInstall(string $composerBinary, InstallOptions ...$options): void - { - $appConfig = file_get_contents(config_path('app.php')); - - file_put_contents(config_path('app.php'), str_replace(<<<'PHP' - /* - * Package Service Providers... - */ -PHP.PHP_EOL, <<hasComposerPackage('filament/filament')) { + warning('Filament Admin Panel is not installed.'); - spin(function () use ($composerBinary) { - if (! $this->hasComposerPackage('filament/filament')) { - $this->requireComposerPackages(['filament/filament'], $composerBinary); - } + spin(function () use ($composerBinary) { + $this->requireComposerPackages(['filament/filament'], $composerBinary); - (new Process([ - $this->phpBinary(), - 'artisan', - 'filament:install', - '--panels', - '--force', - '--quiet', - ], base_path())) + (new Process([$this->phpBinary(), 'artisan', 'filament:install', '--panels', '--force', '--quiet'], base_path())) ->setTimeout(null) ->run(function ($type, $output) { (new BufferedOutput)->write($output); }); }, message: 'Installing Filament Admin Panel...'); - \Laravel\Prompts\info('Filament Admin Panel has been installed successfully!'); + \Laravel\Prompts\info('Filament Admin Panel has been installed successfully!'); + } + + if (! in_array(InstallOptions::Pest, $options)) { + return; + } + + if ($this->hasComposerPackage('pestphp/pest')) { + return; + } + + warning('Pest is not installed.'); + + spin(function () { + if ($this->hasComposerPackage('phpunit/phpunit')) { + $this->removeComposerDevPackages(['phpunit/phpunit']); + } + + $this->requireComposerDevPackages(['pestphp/pest:^2.0', 'pestphp/pest-plugin-laravel:^2.0']); + + $stubs = __DIR__.'/../../../../stubs/filament/pest-tests'; + + 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')); + }, message: 'Installing Pest...'); } - /** - * Copy the Socialstream models and their factories to the base "app" directory. - */ protected function copyModelsAndFactories(): static { parent::copyModelsAndFactories(); @@ -72,11 +68,18 @@ protected function copyModelsAndFactories(): static return $this; } - /** - * Copy the Socialstream test files to the apps "tests" directory for the given test runner. - */ + protected function installServiceProviders(): static + { + parent::installServiceProviders(); + + ServiceProvider::addProviderToBootstrapFile('JoelButcher\Socialstream\Filament\SocialstreamPanelProvider'); + + return $this; + } + protected function copyTests(TestRunner $testRunner): static { + copy(from: match ($testRunner) { TestRunner::Pest => __DIR__.'/../../../../stubs/filament/pest-tests/SocialstreamRegistrationTest.php', TestRunner::PhpUnit => __DIR__.'/../../../../stubs/filament/tests/SocialstreamRegistrationTest.php', diff --git a/src/Installer/Drivers/Jetstream/InertiaDriver.php b/src/Installer/Drivers/Jetstream/InertiaDriver.php index 8690b91a..1698447f 100644 --- a/src/Installer/Drivers/Jetstream/InertiaDriver.php +++ b/src/Installer/Drivers/Jetstream/InertiaDriver.php @@ -8,17 +8,11 @@ class InertiaDriver extends JetstreamDriver { - /** - * Specify the stack used by this installer. - */ protected static function stack(): JetstreamInstallStack { return JetstreamInstallStack::Inertia; } - /** - * Define the resource directories that should be checked for existence for the stack. - */ protected static function directoriesToCreateForStack(): array { return [ @@ -29,9 +23,6 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/jetstream/inertia/resources/js/Pages/Auth/Login.vue', resource_path('js/Pages/Auth/Login.vue')); @@ -40,9 +31,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/jetstream/inertia/resources/js/Pages/Profile/Partials/ConnectedAccountsForm.vue', resource_path('js/Pages/Profile/Partials/ConnectedAccountsForm.vue')); @@ -52,9 +40,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { (new Filesystem)->copyDirectory(__DIR__.'/../../../../stubs/jetstream/inertia/resources/js/Components/SocialstreamIcons', resource_path('js/Components/SocialstreamIcons')); diff --git a/src/Installer/Drivers/Jetstream/JetstreamDriver.php b/src/Installer/Drivers/Jetstream/JetstreamDriver.php index 57b3c4f9..8172d7a9 100644 --- a/src/Installer/Drivers/Jetstream/JetstreamDriver.php +++ b/src/Installer/Drivers/Jetstream/JetstreamDriver.php @@ -4,7 +4,6 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\File; -use Illuminate\Support\Str; use JoelButcher\Socialstream\Installer\Drivers\Driver; use JoelButcher\Socialstream\Installer\Enums\InstallOptions; use JoelButcher\Socialstream\Installer\Enums\JetstreamInstallStack; @@ -23,9 +22,6 @@ abstract class JetstreamDriver extends Driver */ abstract protected static function stack(): JetstreamInstallStack; - /** - * Copy the Socialstream routes. - */ protected function installRoutes(): static { $stack = static::stack()->value; @@ -37,9 +33,6 @@ protected function installRoutes(): static return $this; } - /** - * Check for, and install Laravel Jetstream, if required. - */ protected function ensureDependenciesAreInstalled(string $composerBinary, InstallOptions ...$options): void { if (file_exists(config_path('jetstream.php'))) { @@ -76,9 +69,6 @@ protected function ensureDependenciesAreInstalled(string $composerBinary, Instal \Laravel\Prompts\info('Laravel Jetstream has been installed successfully!'); } - /** - * Copy the Socialstream models to the base "app" directory. - */ protected function copyModelsAndFactories(): static { parent::copyModelsAndFactories(); @@ -89,9 +79,6 @@ protected function copyModelsAndFactories(): static return $this; } - /** - * Copy the Socialstream test files to the apps "tests" directory for the stacks given test runner. - */ protected function copyTests(TestRunner $testRunner): static { copy(from: match ($testRunner) { @@ -120,9 +107,6 @@ protected function ensureTeamsCompatibility(InstallOptions ...$options): static return $this; } - /** - * Execute a script to be run post-installation of the stack. - */ protected function postInstall(string $composerBinary, InstallOptions ...$options): void { $this->ensureTeamsCompatibility(...$options); diff --git a/src/Installer/Drivers/Jetstream/LivewireDriver.php b/src/Installer/Drivers/Jetstream/LivewireDriver.php index bf58fc5c..0372bccc 100644 --- a/src/Installer/Drivers/Jetstream/LivewireDriver.php +++ b/src/Installer/Drivers/Jetstream/LivewireDriver.php @@ -8,17 +8,11 @@ class LivewireDriver extends JetstreamDriver { - /** - * Specify the stack used by this installer. - */ protected static function stack(): JetstreamInstallStack { return JetstreamInstallStack::Livewire; } - /** - * Define the resource directories that should be checked for existence for the stack. - */ protected static function directoriesToCreateForStack(): array { return [ @@ -29,9 +23,6 @@ protected static function directoriesToCreateForStack(): array ]; } - /** - * Copy the auth views to the app "resources" directory for the given stack. - */ public function copyAuthViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/jetstream/livewire/resources/views/auth/login.blade.php', resource_path('views/auth/login.blade.php')); @@ -40,9 +31,6 @@ public function copyAuthViews(InstallOptions ...$options): static return $this; } - /** - * Copy the profile views to the app "resources" directory for the given stack. - */ public function copyProfileViews(InstallOptions ...$options): static { copy(__DIR__.'/../../../../stubs/jetstream/livewire/resources/views/profile/connected-accounts-form.blade.php', resource_path('views/profile/connected-accounts-form.blade.php')); @@ -52,9 +40,6 @@ public function copyProfileViews(InstallOptions ...$options): static return $this; } - /** - * Copy the Socialstream components to the app "resources" directory for the given stack. - */ public function copySocialstreamComponents(InstallOptions ...$options): static { (new Filesystem)->copyDirectory(__DIR__.'/../../../../stubs/jetstream/livewire/resources/views/components/socialstream-icons', resource_path('views/components/socialstream-icons')); diff --git a/src/Socialstream.php b/src/Socialstream.php index 12367b0b..64a041e8 100644 --- a/src/Socialstream.php +++ b/src/Socialstream.php @@ -34,11 +34,15 @@ class Socialstream /** * The user model that should be used by Jetstream. + * + * @var class-string */ public static string $userModel = 'App\\Models\\User'; /** * The user model that should be used by Jetstream. + * + * @var class-string */ public static string $connectedAccountModel = 'App\\Models\\ConnectedAccount'; @@ -81,7 +85,7 @@ public static function useUserModel(string $model): static /** * Determine whether Socialstream is enabled in the application. */ - public static function enabled(callable|bool $callback = null): bool + public static function enabled(callable|bool|null $callback = null): bool { if (is_callable($callback)) { static::$enabled = $callback(); diff --git a/src/SocialstreamServiceProvider.php b/src/SocialstreamServiceProvider.php index 440c1ca3..8840d923 100644 --- a/src/SocialstreamServiceProvider.php +++ b/src/SocialstreamServiceProvider.php @@ -33,6 +33,7 @@ use JoelButcher\Socialstream\Resolvers\OAuth\LinkedInOAuth2RefreshResolver; use JoelButcher\Socialstream\Resolvers\OAuth\SlackOAuth2RefreshResolver; use JoelButcher\Socialstream\Resolvers\OAuth\TwitterOAuth2RefreshResolver; +use Laravel\Fortify\Fortify; use Laravel\Jetstream\Jetstream; use Livewire\Livewire; @@ -68,7 +69,6 @@ protected function registerResponseBindings(): void public function boot(): void { $this->configureDefaults(); - $this->configurePublishing(); $this->configureRoutes(); $this->configureCommands(); $this->configureRefreshTokenResolvers(); @@ -103,38 +103,17 @@ private function configureDefaults(): void Socialstream::generatesProvidersRedirectsUsing(GenerateRedirectForProvider::class); } - /** - * Configure publishing for the package. - */ - private function configurePublishing(): void - { - if (! $this->app->runningInConsole()) { - return; - } - - $this->publishes([ - __DIR__.'/../config/socialstream.php' => config_path('socialstream.php'), - ], 'socialstream-config'); - - $this->publishesMigrations([ - __DIR__.'/../database/migrations/0001_01_01_000001_make_password_nullable_on_users_table.php' => database_path('migrations/0001_01_01_000001_make_password_nullable_on_users_table.php'), - __DIR__.'/../database/migrations/0001_01_01_000002_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000002_create_connected_accounts_table.php'), - ], 'socialstream-migrations'); - - $this->publishes([ - __DIR__.'/../routes/socialstream.php' => base_path('routes/socialstream.php'), - ], 'socialstream-routes'); - - $this->publishes([ - __DIR__.'/../stubs/app/Actions/Socialstream/' => app_path('Actions/Socialstream/'), - ], 'socialstream-actions'); - } - /** * Configure the routes offered by the application. */ private function configureRoutes(): void { + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../routes/socialstream.php' => base_path('routes/socialstream.php'), + ], 'socialstream-routes'); + } + if (! Socialstream::$registersRoutes) { return; } @@ -190,24 +169,28 @@ protected function bootLaravelBreeze(): void return; } - if (class_exists('\App\Providers\VoltServiceProvider')) { - return; - } + $this->publishes([ + __DIR__.'/../config/socialstream.php.php' => config_path('socialstream.php'), + ], 'socialstream-config'); - if ($this->hasComposerPackage('inertiajs/inertia-laravel')) { - $this->publishes(paths: [ - __DIR__.'/../stubs/breeze/inertia/routes/socialstream.php' => base_path('routes/socialstream.php'), - ], groups: 'socialstream-routes'); - } else { - $this->publishes(paths: [ - __DIR__.'/../stubs/breeze/default/routes/socialstream.php' => base_path('routes/socialstream.php'), - ], groups: 'socialstream-routes'); - } + $this->publishes([ + __DIR__.'/../stubs/app/Actions/Socialstream/' => app_path('Actions/Socialstream/'), + ], 'socialstream-actions'); $this->publishesMigrations([ - __DIR__.'/../database/migrations/0001_01_01_000000_create_breeze_users_table.php' => database_path('migrations/0001_01_01_000000_create_users_table.php'), - __DIR__.'/../database/migrations/0001_01_01_000002_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000002_create_connected_accounts_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000000_make_password_nullable_on_users_table.php' => database_path('migrations/0001_01_01_000000_make_password_nullable_on_users_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000001_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000001_create_connected_accounts_table.php'), ], 'socialstream-migrations'); + + if (class_exists('\App\Providers\VoltServiceProvider')) { + return; + } + + $this->publishes($this->hasComposerPackage('inertiajs/inertia-laravel') ? [ + __DIR__.'/../stubs/breeze/inertia/routes/socialstream.php' => base_path('routes/socialstream.php'), + ] : [ + __DIR__.'/../stubs/breeze/default/routes/socialstream.php' => base_path('routes/socialstream.php'), + ], groups: 'socialstream-routes'); } /** @@ -239,11 +222,9 @@ protected function bootLaravelJetstream(): void return; } - if (config('jetstream.stack') === 'inertia') { - $this->publishes([ - __DIR__.'/../routes/inertia.php' => base_path('routes/socialstream.php'), - ], 'socialstream-routes'); - } + $this->publishes([ + __DIR__.'/../config/socialstream.php' => config_path('socialstream.php'), + ], 'socialstream-config'); $this->publishes(array_merge([ __DIR__.'/../stubs/app/Actions/Socialstream/' => app_path('Actions/Socialstream/'), @@ -253,9 +234,15 @@ protected function bootLaravelJetstream(): void ] : []), 'socialstream-actions'); $this->publishesMigrations([ - __DIR__.'/../database/migrations/0001_01_01_000000_create_users_table.php' => database_path('migrations/0001_01_01_000000_create_users_table.php'), - __DIR__.'/../database/migrations/0001_01_01_000002_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000002_create_connected_accounts_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000000_make_password_nullable_on_users_table.php' => database_path('migrations/0001_01_01_000000_make_password_nullable_on_users_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000001_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000001_create_connected_accounts_table.php'), ], 'socialstream-migrations'); + + $this->publishes(config('jetstream.stack') === 'inertia' ? [ + __DIR__.'/../routes/inertia.php' => base_path('routes/socialstream.php'), + ] : [ + __DIR__.'/../routes/socialstream.php' => base_path('routes/socialstream.php'), + ], 'socialstream-routes'); } /** @@ -276,10 +263,14 @@ protected function bootFilament(): void ], 'socialstream-actions'); $this->publishesMigrations([ - __DIR__.'/../database/migrations/0001_01_01_000000_create_users_table.php' => database_path('migrations/0001_01_01_000000_create_users_table.php'), - __DIR__.'/../database/migrations/0001_01_01_000002_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000002_create_connected_accounts_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000000_make_password_nullable_on_users_table.php' => database_path('migrations/0001_01_01_000000_make_password_nullable_on_users_table.php'), + __DIR__.'/../database/migrations/0001_01_01_000001_create_connected_accounts_table.php' => database_path('migrations/0001_01_01_000001_create_connected_accounts_table.php'), ], 'socialstream-migrations'); + $this->publishes([ + __DIR__.'/../routes/socialstream.php' => base_path('routes/socialstream.php'), + ], 'socialstream-routes'); + $this->publishes([ __DIR__.'/../resources/views' => base_path('resources/views/vendor/socialstream'), ], 'socialstream-views'); diff --git a/stubs/app/Providers/SocialstreamServiceProvider.php b/stubs/app/Providers/SocialstreamServiceProvider.php index 894954c6..41e2ee0e 100644 --- a/stubs/app/Providers/SocialstreamServiceProvider.php +++ b/stubs/app/Providers/SocialstreamServiceProvider.php @@ -9,10 +9,14 @@ use App\Actions\Socialstream\ResolveSocialiteUser; use App\Actions\Socialstream\UpdateConnectedAccount; use Illuminate\Support\ServiceProvider; +use JoelButcher\Socialstream\Concerns\ConfirmsFilament; use JoelButcher\Socialstream\Socialstream; +use Laravel\Fortify\Fortify; class SocialstreamServiceProvider extends ServiceProvider { + use ConfirmsFilament; + /** * Register any application services. */ diff --git a/stubs/filament/app/Models/User.php b/stubs/filament/app/Models/User.php index ddccfb65..4a1cbec6 100644 --- a/stubs/filament/app/Models/User.php +++ b/stubs/filament/app/Models/User.php @@ -10,11 +10,10 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use JoelButcher\Socialstream\HasConnectedAccounts; -use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements FilamentUser { - use HasApiTokens, HasFactory, Notifiable; + use HasFactory, Notifiable; use HasConnectedAccounts; /** diff --git a/stubs/filament/pest-tests/ExampleTest.php b/stubs/filament/pest-tests/ExampleTest.php new file mode 100644 index 00000000..8b5843f4 --- /dev/null +++ b/stubs/filament/pest-tests/ExampleTest.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); +}); diff --git a/stubs/filament/pest-tests/ExampleUnitTest.php b/stubs/filament/pest-tests/ExampleUnitTest.php new file mode 100644 index 00000000..44a4f337 --- /dev/null +++ b/stubs/filament/pest-tests/ExampleUnitTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/stubs/filament/pest-tests/Pest.php b/stubs/filament/pest-tests/Pest.php new file mode 100644 index 00000000..e2eb3808 --- /dev/null +++ b/stubs/filament/pest-tests/Pest.php @@ -0,0 +1,48 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/testbench.yaml b/testbench.yaml index c573d800..c5f2395b 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -6,10 +6,9 @@ providers: - JoelButcher\Socialstream\SocialstreamServiceProvider migrations: - - database/migrations/0001_01_01_000001_make_password_nullable_on_users_table.php - - database/migrations/0001_01_01_000002_create_connected_accounts_table.php - - vendor/laravel/fortify/database/migrations - vendor/laravel/jetstream/database/migrations + - database/migrations + - vendor/laravel/fortify/database/migrations workbench: install: false diff --git a/tests/Feature/LoginOnRegistrationTest.php b/tests/Feature/LoginOnRegistrationTest.php index 82c6ea1f..d55131f0 100644 --- a/tests/Feature/LoginOnRegistrationTest.php +++ b/tests/Feature/LoginOnRegistrationTest.php @@ -46,9 +46,9 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('register')); @@ -87,9 +87,9 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('register')); @@ -129,9 +129,9 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', '/random'); diff --git a/tests/Feature/RedirectTest.php b/tests/Feature/RedirectTest.php index 42f8dbe8..2084e0fc 100644 --- a/tests/Feature/RedirectTest.php +++ b/tests/Feature/RedirectTest.php @@ -50,8 +50,8 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + $provider->shouldReceive('user')->andReturn($user); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('login')); @@ -77,8 +77,8 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + $provider->shouldReceive('user')->andReturn($user); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('register')); @@ -106,8 +106,8 @@ ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + $provider->shouldReceive('user')->andReturn($user); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('login')); diff --git a/tests/Feature/SocialstreamTest.php b/tests/Feature/SocialstreamTest.php index 925c89d3..93805750 100644 --- a/tests/Feature/SocialstreamTest.php +++ b/tests/Feature/SocialstreamTest.php @@ -5,12 +5,15 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; use JoelButcher\Socialstream\Contracts\GeneratesProviderRedirect; +use JoelButcher\Socialstream\Providers; use JoelButcher\Socialstream\Socialstream; +use Laravel\Fortify\Features; use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Two\GithubProvider; use Laravel\Socialite\Two\User as SocialiteUser; @@ -85,9 +88,9 @@ public function generate(string $provider): RedirectResponse ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('register')); @@ -139,9 +142,9 @@ public function generate(string $provider): RedirectResponse ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('login')); @@ -151,6 +154,61 @@ public function generate(string $provider): RedirectResponse $this->assertAuthenticated(); }); +test('existing users with 2FA enabled are redirected', function (): void { + Config::set('socialstream.providers', [Providers::github()]); + Config::set('fortify.features', array_merge(Config::get('fortify.features'), [ + Features::twoFactorAuthentication(options: [ + 'confirm' => false, + 'confirmPassword' => true, + ]), + ])); + + $user = Socialstream::$userModel::create([ + 'name' => 'Joel Butcher', + 'email' => 'joel@socialstream.dev', + 'password' => Hash::make('password'), + 'two_factor_secret' => 'foo', + 'two_factor_recovery_codes' => 'bar', + ]); + + $user->connectedAccounts()->create([ + 'provider' => 'github', + 'provider_id' => $githubId = fake()->numerify('########'), + 'email' => 'joel@socialstream.dev', + 'token' => Str::random(64), + ]); + + $this->assertDatabaseHas('users', ['email' => 'joel@socialstream.dev']); + $this->assertDatabaseHas('connected_accounts', [ + 'provider' => 'github', + 'provider_id' => $githubId, + 'email' => 'joel@socialstream.dev', + ]); + + $user = (new SocialiteUser()) + ->map([ + 'id' => $githubId, + 'nickname' => 'joel', + 'name' => 'Joel', + 'email' => 'joel@socialstream.dev', + 'avatar' => null, + 'avatar_original' => null, + ]) + ->setToken('user-token') + ->setRefreshToken('refresh-token') + ->setExpiresIn(3600); + + $provider = Mockery::mock(GithubProvider::class); + $provider->shouldReceive('user')->andReturn($user); + + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); + + Session::put('socialstream.previous_url', route('login')); + + get('http://localhost/oauth/github/callback') + ->assertRedirect(route('two-factor.login')); +}); + test('authenticated users can link to provider', function (): void { $this->actingAs(User::create([ 'name' => 'Joel Butcher', @@ -175,9 +233,9 @@ public function generate(string $provider): RedirectResponse ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); get('http://localhost/oauth/github/callback') ->assertRedirect('/user/profile'); @@ -193,7 +251,7 @@ public function generate(string $provider): RedirectResponse test('users can be authenticated with the same provider if they change the email associated with their user', function () { $user = User::create([ 'name' => 'Joel Butcher', - 'email' => 'joel@socialstream.com', + 'email' => 'joel@socialstream.dev', 'password' => Hash::make('password'), ]); @@ -219,9 +277,9 @@ public function generate(string $provider): RedirectResponse ->setExpiresIn(3600); $provider = Mockery::mock(GithubProvider::class); - $provider->shouldReceive('user')->once()->andReturn($user); + $provider->shouldReceive('user')->andReturn($user); - Socialite::shouldReceive('driver')->once()->with('github')->andReturn($provider); + Socialite::shouldReceive('driver')->with('github')->andReturn($provider); Session::put('socialstream.previous_url', route('login')); diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php index 720c34cb..ae26f759 100644 --- a/tests/Fixtures/User.php +++ b/tests/Fixtures/User.php @@ -11,10 +11,7 @@ class User extends BaseUser { use HasApiTokens, HasTeams, HasProfilePhoto; - /** - * The attributes that aren't mass assignable. - * - * @var array - */ protected $guarded = []; + + protected $fillable = []; }