diff --git a/config/services.php b/config/services.php index 53e6dc3..2ff9869 100644 --- a/config/services.php +++ b/config/services.php @@ -6,5 +6,12 @@ "redirect" => env("SIGN_IN_WITH_APPLE_REDIRECT"), "client_id" => env("SIGN_IN_WITH_APPLE_CLIENT_ID"), "client_secret" => env("SIGN_IN_WITH_APPLE_CLIENT_SECRET"), + // Auto-register package routes + "routes" => [ + "enabled" => true, + "redirect_route" => "apple/redirect", + "callback_route" => "apple/callback", + "callback_redirect" => "/", + ], ], ]; diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..c02ddcc --- /dev/null +++ b/routes/web.php @@ -0,0 +1,19 @@ + ['web'], +], function () { + $redirectPath = config('services.sign_in_with_apple.routes.redirect_route', 'apple/redirect'); + $callbackPath = config('services.sign_in_with_apple.routes.callback_route', 'apple/callback'); + + Route::get($redirectPath, [AppleSignInController::class, 'redirect']) + ->name('apple.redirect'); + + Route::post($callbackPath, [AppleSignInController::class, 'callback']) + ->withoutMiddleware([VerifyCsrfToken::class]) + ->name('apple.callback'); +}); diff --git a/src/Events/AppleSignInCallback.php b/src/Events/AppleSignInCallback.php new file mode 100644 index 0000000..a1a5bc4 --- /dev/null +++ b/src/Events/AppleSignInCallback.php @@ -0,0 +1,18 @@ +scopes(['name', 'email']) + ->redirect(); + } + + /** + * Handle the callback from Apple. + * + * Dispatches an AppleSignInCallback event with the Socialite user, + * then redirects to a configurable route. Listen for the event + * to persist or process the authenticated user. + */ + public function callback(Request $request): RedirectResponse + { + $user = Socialite::driver('sign-in-with-apple')->user(); + + AppleSignInCallback::dispatch($user); + + $redirect = config( + 'services.sign_in_with_apple.routes.callback_redirect', + '/', + ); + + return redirect()->to($redirect); + } +} diff --git a/src/Providers/ServiceProvider.php b/src/Providers/ServiceProvider.php index 2212cf6..b7e8a7f 100644 --- a/src/Providers/ServiceProvider.php +++ b/src/Providers/ServiceProvider.php @@ -13,6 +13,7 @@ class ServiceProvider extends LaravelServiceProvider public function boot() { + $this->loadRoutesIf(config('services.sign_in_with_apple.routes.enabled', true)); $this->bootSocialiteDriver(); $this->bootBladeDirective(); } @@ -30,6 +31,16 @@ protected function registerConfiguration() ); } + /** + * Load package routes if enabled in config. + */ + protected function loadRoutesIf(bool $enabled): void + { + if ($enabled) { + $this->loadRoutesFrom(__DIR__ . '/../../routes/web.php'); + } + } + public function bootSocialiteDriver() { $socialite = $this->app->make(Factory::class); diff --git a/tests/Unit/RouteRegistrationTest.php b/tests/Unit/RouteRegistrationTest.php new file mode 100644 index 0000000..c117326 --- /dev/null +++ b/tests/Unit/RouteRegistrationTest.php @@ -0,0 +1,146 @@ +app['router']->getRoutes(); + + $redirectRoute = $routes->getByName('apple.redirect'); + + $this->assertNotNull($redirectRoute); + $this->assertEquals('apple/redirect', $redirectRoute->uri); + $this->assertTrue(in_array('GET', $redirectRoute->methods)); + } + + public function testAppleCallbackRouteIsRegistered(): void + { + $routes = $this->app['router']->getRoutes(); + + $callbackRoute = $routes->getByName('apple.callback'); + + $this->assertNotNull($callbackRoute); + $this->assertEquals('apple/callback', $callbackRoute->uri); + $this->assertTrue(in_array('POST', $callbackRoute->methods)); + } + + public function testRedirectRouteUsesAppleSignInController(): void + { + $routes = $this->app['router']->getRoutes(); + $route = $routes->getByName('apple.redirect'); + + $controller = $route->getAction('controller'); + $this->assertStringContainsString('AppleSignInController', $controller); + $this->assertStringContainsString('redirect', $controller); + } + + public function testCallbackRouteUsesAppleSignInController(): void + { + $routes = $this->app['router']->getRoutes(); + $route = $routes->getByName('apple.callback'); + + $controller = $route->getAction('controller'); + $this->assertStringContainsString('AppleSignInController', $controller); + $this->assertStringContainsString('callback', $controller); + } + + public function testRoutesAreProtectedWithWebMiddleware(): void + { + $routes = $this->app['router']->getRoutes(); + + $redirectRoute = $routes->getByName('apple.redirect'); + $callbackRoute = $routes->getByName('apple.callback'); + + $this->assertTrue(in_array('web', $redirectRoute->middleware())); + $this->assertTrue(in_array('web', $callbackRoute->middleware())); + } + + public function testCallbackRouteExcludesCsrfVerification(): void + { + $routes = $this->app['router']->getRoutes(); + $callbackRoute = $routes->getByName('apple.callback'); + + $excludedMiddleware = $callbackRoute->excludedMiddleware(); + $this->assertContains(VerifyCsrfToken::class, $excludedMiddleware); + } + + public function testRoutesAreNotRegisteredWhenDisabled(): void + { + $this->app['config']->set('services.sign_in_with_apple.routes.enabled', false); + + // Clear routes and re-boot the service provider with routes disabled + $this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection()); + + $provider = new \GeneaLabs\LaravelSignInWithApple\Providers\ServiceProvider($this->app); + $provider->boot(); + + $routes = $this->app['router']->getRoutes(); + $routes->refreshNameLookups(); + + $this->assertNull($routes->getByName('apple.redirect')); + $this->assertNull($routes->getByName('apple.callback')); + } + + public function testRedirectRoutePathCanBeCustomized(): void + { + $this->app['config']->set('services.sign_in_with_apple.routes.redirect_route', 'auth/apple/login'); + + $this->reloadRoutes(); + + $routes = $this->app['router']->getRoutes(); + $redirectRoute = $routes->getByName('apple.redirect'); + + $this->assertNotNull($redirectRoute); + $this->assertEquals('auth/apple/login', $redirectRoute->uri); + } + + public function testCallbackRoutePathCanBeCustomized(): void + { + $this->app['config']->set('services.sign_in_with_apple.routes.callback_route', 'auth/apple/handle'); + + $this->reloadRoutes(); + + $routes = $this->app['router']->getRoutes(); + $callbackRoute = $routes->getByName('apple.callback'); + + $this->assertNotNull($callbackRoute); + $this->assertEquals('auth/apple/handle', $callbackRoute->uri); + } + + /** + * Clear existing routes and reload the package route file. + */ + private function reloadRoutes(): void + { + $this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection()); + + require __DIR__ . '/../../routes/web.php'; + + $this->app['router']->getRoutes()->refreshNameLookups(); + } + + public function testControllerCanBeOverriddenByApplication(): void + { + $this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection()); + + // Simulate an app overriding the callback route with a custom controller + $this->app['router']->group(['middleware' => ['web']], function ($router) { + $router->post('apple/callback', [\GeneaLabs\LaravelSignInWithApple\Tests\Fixtures\Http\Controllers\SiwaController::class, 'callback']) + ->name('apple.callback'); + }); + + $routes = $this->app['router']->getRoutes(); + $routes->refreshNameLookups(); + $callbackRoute = $routes->getByName('apple.callback'); + + $this->assertNotNull($callbackRoute); + $controller = $callbackRoute->getAction('controller'); + $this->assertStringContainsString('SiwaController', $controller); + } +}