Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.x] Support Laravel Passport #398

Draft
wants to merge 12 commits into
base: 2.x
Choose a base branch
from
Draft
8 changes: 8 additions & 0 deletions src/Console/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class InstallCommand extends Command implements PromptsForMissingInput
{--ssr : Indicates if Inertia SSR support should be installed}
{--typescript : Indicates if TypeScript is preferred for the Inertia stack}
{--eslint : Indicates if ESLint with Prettier should be installed}
{--oauth : Indicates that OAuth support via Laravel Passport should be installed for the API stack}
{--composer=global : Absolute path to the Composer binary which should be used to install packages}';

/**
Expand Down Expand Up @@ -416,6 +417,13 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
));
}

if (in_array($stack, ['api', 'blade', 'react', 'vue'])) {
$input->setOption('oauth', confirm(
label: 'Would you like OAuth support via Laravel Passport?',
default: false
));
}

$input->setOption('pest', select(
label: 'Which testing framework do you prefer?',
options: ['Pest', 'PHPUnit'],
Expand Down
21 changes: 17 additions & 4 deletions src/Console/InstallsApiStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ trait InstallsApiStack
*/
protected function installApiStack()
{
$this->runCommands(['php artisan install:api']);
$this->runCommands([
$this->option('oauth') ? 'php artisan install:api --passport' : 'php artisan install:api',
]);

$files = new Filesystem;

Expand All @@ -28,9 +30,15 @@ protected function installApiStack()
'verified' => '\App\Http\Middleware\EnsureEmailIsVerified::class',
]);

$this->installMiddleware([
'\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class',
], 'api', 'prepend');
if ($this->option('oauth')) {
$this->installMiddleware([
'\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class',
], 'web', 'append');
} else {
$this->installMiddleware([
'\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class',
], 'api', 'prepend');
}

// Requests...
$files->ensureDirectoryExists(app_path('Http/Requests/Auth'));
Expand All @@ -44,6 +52,11 @@ protected function installApiStack()
copy(__DIR__.'/../../stubs/api/routes/web.php', base_path('routes/web.php'));
copy(__DIR__.'/../../stubs/api/routes/auth.php', base_path('routes/auth.php'));

// OAuth...
if ($this->option('oauth')) {
$this->replaceInFile('auth:sanctum', 'auth:api', base_path('routes/api.php'));
}

// Configuration...
$files->copyDirectory(__DIR__.'/../../stubs/api/config', config_path());

Expand Down
10 changes: 10 additions & 0 deletions src/Console/InstallsBladeStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ protected function installBladeStack()
] + $packages;
});

// Install Passport...
if ($this->option('oauth')) {
if (! $this->requireComposerPackages(['laravel/passport:^13.0'])) {
return 1;
}

// Providers...
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/default/app/Providers', app_path('Providers'));
}

// Controllers...
(new Filesystem)->ensureDirectoryExists(app_path('Http/Controllers'));
(new Filesystem)->copyDirectory(__DIR__.'/../../stubs/default/app/Http/Controllers', app_path('Http/Controllers'));
Expand Down
12 changes: 10 additions & 2 deletions src/Console/InstallsInertiaStacks.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ trait InstallsInertiaStacks
protected function installInertiaVueStack()
{
// Install Inertia...
if (! $this->requireComposerPackages(['inertiajs/inertia-laravel:^2.0', 'laravel/sanctum:^4.0', 'tightenco/ziggy:^2.0'])) {
if (! $this->requireComposerPackages([
'inertiajs/inertia-laravel:^2.0',
$this->option('oauth') ? 'laravel/passport:^13.0' : 'laravel/sanctum:^4.0',
'tightenco/ziggy:^2.0',
])) {
return 1;
}

Expand Down Expand Up @@ -226,7 +230,11 @@ protected function installInertiaVueSsrStack()
protected function installInertiaReactStack()
{
// Install Inertia...
if (! $this->requireComposerPackages(['inertiajs/inertia-laravel:^2.0', 'laravel/sanctum:^4.0', 'tightenco/ziggy:^2.0'])) {
if (! $this->requireComposerPackages([
'inertiajs/inertia-laravel:^2.0',
$this->option('oauth') ? 'laravel/passport:^13.0' : 'laravel/sanctum:^4.0',
'tightenco/ziggy:^2.0',
])) {
return 1;
}

Expand Down
25 changes: 25 additions & 0 deletions stubs/default/app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::viewPrefix('auth.oauth.');
}
}
53 changes: 53 additions & 0 deletions stubs/default/resources/views/auth/oauth/authorize.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<x-guest-layout>
<div class="mb-4 text-gray-600 text-center">
<p><strong>{{ $user->name }}</strong></p>
<p class="text-sm">{{ $user->email }}</p>
</div>

<div class="mb-4 text-sm text-gray-600">
{{ __(':client is requesting permission to access your account.', ['client' => $client->name]) }}
</div>

@if (count($scopes) > 0)
<div class="mb-4 text-sm text-gray-600">
<p class="pb-1">{{ __('This application will be able to:') }}</p>

<ul class="list-inside list-disc">
@foreach ($scopes as $scope)
<li>{{ $scope->description }}</li>
@endforeach
</ul>
</div>
@endif

<div class="flex flex-row-reverse gap-3 mt-4 flex-wrap items-center">
<form method="POST" action="{{ route('passport.authorizations.approve') }}">
@csrf

<input type="hidden" name="state" value="{{ $request->state }}">
<input type="hidden" name="client_id" value="{{ $client->getKey() }}">
<input type="hidden" name="auth_token" value="{{ $authToken }}">

<x-primary-button>
{{ __('Authorize') }}
</x-primary-button>
</form>

<form method="POST" action="{{ route('passport.authorizations.deny') }}">
@csrf
@method('DELETE')

<input type="hidden" name="state" value="{{ $request->state }}">
<input type="hidden" name="client_id" value="{{ $client->getKey() }}">
<input type="hidden" name="auth_token" value="{{ $authToken }}">

<x-secondary-button type="submit">
{{ __('Decline') }}
</x-secondary-button>
</form>

<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ $request->fullUrlWithQuery(['prompt' => 'login']) }}">
{{ __('Log into another account') }}
</a>
</div>
</x-guest-layout>
6 changes: 6 additions & 0 deletions stubs/inertia-common/app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;
use Laravel\Passport\Passport;

class AppServiceProvider extends ServiceProvider
{
Expand All @@ -21,5 +23,9 @@ public function register(): void
public function boot(): void
{
Vite::prefetch(concurrency: 3);

if (class_exists(Passport::class)) {
Passport::authorizationView(fn ($params) => Inertia::render('Auth/OAuth/Authorize', $params));
}
}
}
88 changes: 88 additions & 0 deletions stubs/inertia-react-ts/resources/js/Pages/Auth/OAuth/Authorize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { FormEventHandler } from 'react';
import GuestLayout from '@/Layouts/GuestLayout';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import { Head, Link, useForm } from '@inertiajs/react';

export default function Authorize({
user,
client,
scopes,
authToken,
}: {
user: { name: string; email: string },
client: { id: string; name: string },
scopes: { description: string }[],
authToken: string,
}) {
const { post, processing, transform } = useForm({
state: route().params.state,
client_id: client.id,
auth_token: authToken,
});

const approve: FormEventHandler = (e) => {
e.preventDefault();

post(route('passport.authorizations.approve'));
};

const deny: FormEventHandler = (e) => {
e.preventDefault();

transform((data) => ({
...data,
_method: 'delete',
}));

post(route('passport.authorizations.deny'));
};

return (
<GuestLayout>
<Head title="Authorization Request" />

<div className="mb-4 text-gray-600 text-center">
<p>
<strong>{user.name}</strong>
</p>
<p className="text-sm">{user.email}</p>
</div>

<div className="mb-4 text-sm text-gray-600">
<strong>{client.name}</strong> is requesting permission to access your account.
</div>

{scopes.length > 0 && (
<div className="mb-4 text-sm text-gray-600">
<p className="pb-1">This application will be able to:</p>

<ul className="list-inside list-disc">
{scopes.map((scope) => (
<li>{scope.description}</li>
))}
</ul>
</div>
)}

<div className="flex flex-row-reverse gap-3 mt-4 flex-wrap items-center">
<form onSubmit={approve}>
<PrimaryButton disabled={processing}>Authorize</PrimaryButton>
</form>

<form onSubmit={deny}>
<SecondaryButton type="submit" disabled={processing}>
Decline
</SecondaryButton>
</form>

<Link
href={route('passport.authorizations.authorize', { ...route().params, prompt: 'login' })}
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Log into another account
</Link>
</div>
</GuestLayout>
);
}
77 changes: 77 additions & 0 deletions stubs/inertia-react/resources/js/Pages/Auth/OAuth/Authorize.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import GuestLayout from '@/Layouts/GuestLayout';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import { Head, Link, useForm } from '@inertiajs/react';

export default function Authorize({ user, client, scopes, authToken }) {
const { post, processing, transform } = useForm({
state: route().params.state,
client_id: client.id,
auth_token: authToken,
});

const approve = (e) => {
e.preventDefault();

post(route('passport.authorizations.approve'));
};

const deny = (e) => {
e.preventDefault();

transform((data) => ({
...data,
_method: 'delete',
}));

post(route('passport.authorizations.deny'));
};

return (
<GuestLayout>
<Head title="Authorization Request" />

<div className="mb-4 text-gray-600 text-center">
<p>
<strong>{user.name}</strong>
</p>
<p className="text-sm">{user.email}</p>
</div>

<div className="mb-4 text-sm text-gray-600">
<strong>{client.name}</strong> is requesting permission to access your account.
</div>

{scopes.length > 0 && (
<div className="mb-4 text-sm text-gray-600">
<p className="pb-1">This application will be able to:</p>

<ul className="list-inside list-disc">
{scopes.map((scope) => (
<li>{scope.description}</li>
))}
</ul>
</div>
)}

<div className="flex flex-row-reverse gap-3 mt-4 flex-wrap items-center">
<form onSubmit={approve}>
<PrimaryButton disabled={processing}>Authorize</PrimaryButton>
</form>

<form onSubmit={deny}>
<SecondaryButton type="submit" disabled={processing}>
Decline
</SecondaryButton>
</form>

<Link
href={route('passport.authorizations.authorize', { ...route().params, prompt: 'login' })}
className="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
>
Log into another account
</Link>
</div>
</GuestLayout>
);
}
Loading