Skip to content

Commit a876489

Browse files
authored
Merge pull request #66 from mikebronner/feat/26-package-routes-controllers
feat(routes): Auto-register Apple Sign In routes and controller
2 parents 3488158 + b09def2 commit a876489

File tree

6 files changed

+244
-0
lines changed

6 files changed

+244
-0
lines changed

config/services.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@
66
"redirect" => env("SIGN_IN_WITH_APPLE_REDIRECT"),
77
"client_id" => env("SIGN_IN_WITH_APPLE_CLIENT_ID"),
88
"client_secret" => env("SIGN_IN_WITH_APPLE_CLIENT_SECRET"),
9+
// Auto-register package routes
10+
"routes" => [
11+
"enabled" => true,
12+
"redirect_route" => "apple/redirect",
13+
"callback_route" => "apple/callback",
14+
"callback_redirect" => "/",
15+
],
916
],
1017
];

routes/web.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use GeneaLabs\LaravelSignInWithApple\Http\Controllers\AppleSignInController;
4+
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
5+
use Illuminate\Support\Facades\Route;
6+
7+
Route::group([
8+
'middleware' => ['web'],
9+
], function () {
10+
$redirectPath = config('services.sign_in_with_apple.routes.redirect_route', 'apple/redirect');
11+
$callbackPath = config('services.sign_in_with_apple.routes.callback_route', 'apple/callback');
12+
13+
Route::get($redirectPath, [AppleSignInController::class, 'redirect'])
14+
->name('apple.redirect');
15+
16+
Route::post($callbackPath, [AppleSignInController::class, 'callback'])
17+
->withoutMiddleware([VerifyCsrfToken::class])
18+
->name('apple.callback');
19+
});

src/Events/AppleSignInCallback.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace GeneaLabs\LaravelSignInWithApple\Events;
4+
5+
use Illuminate\Foundation\Events\Dispatchable;
6+
use Illuminate\Queue\SerializesModels;
7+
use Laravel\Socialite\Contracts\User;
8+
9+
class AppleSignInCallback
10+
{
11+
use Dispatchable;
12+
use SerializesModels;
13+
14+
public function __construct(
15+
public readonly User $user,
16+
) {
17+
}
18+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace GeneaLabs\LaravelSignInWithApple\Http\Controllers;
4+
5+
use GeneaLabs\LaravelSignInWithApple\Events\AppleSignInCallback;
6+
use Illuminate\Http\RedirectResponse;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Routing\Controller;
9+
use Laravel\Socialite\Facades\Socialite;
10+
11+
class AppleSignInController extends Controller
12+
{
13+
/**
14+
* Redirect to Apple for authentication.
15+
*/
16+
public function redirect(): RedirectResponse
17+
{
18+
return Socialite::driver('sign-in-with-apple')
19+
->scopes(['name', 'email'])
20+
->redirect();
21+
}
22+
23+
/**
24+
* Handle the callback from Apple.
25+
*
26+
* Dispatches an AppleSignInCallback event with the Socialite user,
27+
* then redirects to a configurable route. Listen for the event
28+
* to persist or process the authenticated user.
29+
*/
30+
public function callback(Request $request): RedirectResponse
31+
{
32+
$user = Socialite::driver('sign-in-with-apple')->user();
33+
34+
AppleSignInCallback::dispatch($user);
35+
36+
$redirect = config(
37+
'services.sign_in_with_apple.routes.callback_redirect',
38+
'/',
39+
);
40+
41+
return redirect()->to($redirect);
42+
}
43+
}

src/Providers/ServiceProvider.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class ServiceProvider extends LaravelServiceProvider
1313

1414
public function boot()
1515
{
16+
$this->loadRoutesIf(config('services.sign_in_with_apple.routes.enabled', true));
1617
$this->bootSocialiteDriver();
1718
$this->bootBladeDirective();
1819
}
@@ -30,6 +31,16 @@ protected function registerConfiguration()
3031
);
3132
}
3233

34+
/**
35+
* Load package routes if enabled in config.
36+
*/
37+
protected function loadRoutesIf(bool $enabled): void
38+
{
39+
if ($enabled) {
40+
$this->loadRoutesFrom(__DIR__ . '/../../routes/web.php');
41+
}
42+
}
43+
3344
public function bootSocialiteDriver()
3445
{
3546
$socialite = $this->app->make(Factory::class);
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace GeneaLabs\LaravelSignInWithApple\Tests\Unit;
4+
5+
use GeneaLabs\LaravelSignInWithApple\Http\Controllers\AppleSignInController;
6+
use GeneaLabs\LaravelSignInWithApple\Tests\UnitTestCase;
7+
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
8+
9+
class RouteRegistrationTest extends UnitTestCase
10+
{
11+
public function testAppleRedirectRouteIsRegistered(): void
12+
{
13+
$routes = $this->app['router']->getRoutes();
14+
15+
$redirectRoute = $routes->getByName('apple.redirect');
16+
17+
$this->assertNotNull($redirectRoute);
18+
$this->assertEquals('apple/redirect', $redirectRoute->uri);
19+
$this->assertTrue(in_array('GET', $redirectRoute->methods));
20+
}
21+
22+
public function testAppleCallbackRouteIsRegistered(): void
23+
{
24+
$routes = $this->app['router']->getRoutes();
25+
26+
$callbackRoute = $routes->getByName('apple.callback');
27+
28+
$this->assertNotNull($callbackRoute);
29+
$this->assertEquals('apple/callback', $callbackRoute->uri);
30+
$this->assertTrue(in_array('POST', $callbackRoute->methods));
31+
}
32+
33+
public function testRedirectRouteUsesAppleSignInController(): void
34+
{
35+
$routes = $this->app['router']->getRoutes();
36+
$route = $routes->getByName('apple.redirect');
37+
38+
$controller = $route->getAction('controller');
39+
$this->assertStringContainsString('AppleSignInController', $controller);
40+
$this->assertStringContainsString('redirect', $controller);
41+
}
42+
43+
public function testCallbackRouteUsesAppleSignInController(): void
44+
{
45+
$routes = $this->app['router']->getRoutes();
46+
$route = $routes->getByName('apple.callback');
47+
48+
$controller = $route->getAction('controller');
49+
$this->assertStringContainsString('AppleSignInController', $controller);
50+
$this->assertStringContainsString('callback', $controller);
51+
}
52+
53+
public function testRoutesAreProtectedWithWebMiddleware(): void
54+
{
55+
$routes = $this->app['router']->getRoutes();
56+
57+
$redirectRoute = $routes->getByName('apple.redirect');
58+
$callbackRoute = $routes->getByName('apple.callback');
59+
60+
$this->assertTrue(in_array('web', $redirectRoute->middleware()));
61+
$this->assertTrue(in_array('web', $callbackRoute->middleware()));
62+
}
63+
64+
public function testCallbackRouteExcludesCsrfVerification(): void
65+
{
66+
$routes = $this->app['router']->getRoutes();
67+
$callbackRoute = $routes->getByName('apple.callback');
68+
69+
$excludedMiddleware = $callbackRoute->excludedMiddleware();
70+
$this->assertContains(VerifyCsrfToken::class, $excludedMiddleware);
71+
}
72+
73+
public function testRoutesAreNotRegisteredWhenDisabled(): void
74+
{
75+
$this->app['config']->set('services.sign_in_with_apple.routes.enabled', false);
76+
77+
// Clear routes and re-boot the service provider with routes disabled
78+
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());
79+
80+
$provider = new \GeneaLabs\LaravelSignInWithApple\Providers\ServiceProvider($this->app);
81+
$provider->boot();
82+
83+
$routes = $this->app['router']->getRoutes();
84+
$routes->refreshNameLookups();
85+
86+
$this->assertNull($routes->getByName('apple.redirect'));
87+
$this->assertNull($routes->getByName('apple.callback'));
88+
}
89+
90+
public function testRedirectRoutePathCanBeCustomized(): void
91+
{
92+
$this->app['config']->set('services.sign_in_with_apple.routes.redirect_route', 'auth/apple/login');
93+
94+
$this->reloadRoutes();
95+
96+
$routes = $this->app['router']->getRoutes();
97+
$redirectRoute = $routes->getByName('apple.redirect');
98+
99+
$this->assertNotNull($redirectRoute);
100+
$this->assertEquals('auth/apple/login', $redirectRoute->uri);
101+
}
102+
103+
public function testCallbackRoutePathCanBeCustomized(): void
104+
{
105+
$this->app['config']->set('services.sign_in_with_apple.routes.callback_route', 'auth/apple/handle');
106+
107+
$this->reloadRoutes();
108+
109+
$routes = $this->app['router']->getRoutes();
110+
$callbackRoute = $routes->getByName('apple.callback');
111+
112+
$this->assertNotNull($callbackRoute);
113+
$this->assertEquals('auth/apple/handle', $callbackRoute->uri);
114+
}
115+
116+
/**
117+
* Clear existing routes and reload the package route file.
118+
*/
119+
private function reloadRoutes(): void
120+
{
121+
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());
122+
123+
require __DIR__ . '/../../routes/web.php';
124+
125+
$this->app['router']->getRoutes()->refreshNameLookups();
126+
}
127+
128+
public function testControllerCanBeOverriddenByApplication(): void
129+
{
130+
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection());
131+
132+
// Simulate an app overriding the callback route with a custom controller
133+
$this->app['router']->group(['middleware' => ['web']], function ($router) {
134+
$router->post('apple/callback', [\GeneaLabs\LaravelSignInWithApple\Tests\Fixtures\Http\Controllers\SiwaController::class, 'callback'])
135+
->name('apple.callback');
136+
});
137+
138+
$routes = $this->app['router']->getRoutes();
139+
$routes->refreshNameLookups();
140+
$callbackRoute = $routes->getByName('apple.callback');
141+
142+
$this->assertNotNull($callbackRoute);
143+
$controller = $callbackRoute->getAction('controller');
144+
$this->assertStringContainsString('SiwaController', $controller);
145+
}
146+
}

0 commit comments

Comments
 (0)