Migrate from Passport to Sanctum for API

This commit is contained in:
Will Browning 2022-07-21 10:54:36 +01:00
parent d7b40a9259
commit 5bfc5e5e9f
44 changed files with 1539 additions and 1662 deletions

View File

@ -62,8 +62,4 @@ ANONADDY_ADDITIONAL_USERNAME_LIMIT=10
ANONADDY_SIGNING_KEY_FINGERPRINT=
# This is only needed if you will be adding any custom domains. If you do not need it then leave it blank. ANONADDY_DKIM_SIGNING_KEY=/etc/opendkim/keys/example.com/default.private
ANONADDY_DKIM_SIGNING_KEY=
ANONADDY_DKIM_SELECTOR=default
# These details will be displayed after you run php artisan passport:install, you should update accordingly
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value
ANONADDY_DKIM_SELECTOR=default

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ yarn-error.log
.env
.php-cs-fixer.cache
.phpunit.result.cache
ray.php

View File

@ -1086,7 +1086,7 @@ Then update your `.env` file.
ANONADDY_DKIM_SIGNING_KEY=/var/lib/rspamd/dkim/example.com.default.key
```
Then we will generate an app key, migrate the database, link the storage directory, restart the queue and install laravel passport.
Then we will generate an app key, migrate the database, link the storage directory, clear the cache and restart the queue.
```bash
php artisan key:generate
@ -1097,40 +1097,8 @@ php artisan config:cache
php artisan view:cache
php artisan route:cache
php artisan queue:restart
php artisan passport:install
```
Running `passport:install` will output details about a new personal access client, e.g.
```bash
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: MlVp37PNqtN9efBTw2wuenjMnMIlDuKBWK3GZQoJ
Password grant client created successfully.
Client ID: 2
Client secret: ZTvhZCRZMdKUvmwqSmNAfWzAoaRatVWgbCVN2cR2
```
You need to update your `.env` file and add the details for the personal access client:
```
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value
```
So I would enter:
```
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=MlVp37PNqtN9efBTw2wuenjMnMIlDuKBWK3GZQoJ
```
More information can be found in the Laravel documentation for Passport - [https://laravel.com/docs/8.x/passport](https://laravel.com/docs/8.x/passport)
Then run `php artisan config:cache` again to reflect the changes.
We also need to add a cronjob in order to run Laravel's schedules commands.
Type `crontab -e` in the terminal as your `johndoe` user.

View File

@ -1,63 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Mail\TokenExpiringSoon;
use App\Models\User;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class EmailUsersWithTokenExpiringSoon extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'anonaddy:email-users-with-token-expiring-soon';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send an email to users who have an API token that is expiring soon';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
User::whereHas('tokens', function ($query) {
$query->whereDate('expires_at', now()->addWeek())
->where('revoked', false);
})
->get()
->each(function (User $user) {
$this->sendTokenExpiringSoonMail($user);
});
}
protected function sendTokenExpiringSoonMail(User $user)
{
try {
Mail::to($user->email)->send(new TokenExpiringSoon($user));
} catch (Exception $exception) {
$this->error("exception when sending mail to user: {$user->username}", $exception);
report($exception);
}
}
}

View File

@ -25,7 +25,6 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
$schedule->command('anonaddy:reset-bandwidth')->monthlyOn(1, '00:00');
$schedule->command('anonaddy:email-users-with-token-expiring-soon')->dailyAt('12:00');
$schedule->command('anonaddy:check-domains-sending-verification')->daily();
$schedule->command('anonaddy:check-domains-mx-validation')->daily();
$schedule->command('anonaddy:clear-failed-deliveries')->daily();

View File

@ -21,9 +21,9 @@ class AliasController extends Controller
$direction = strpos($sort, '-') === 0 ? 'desc' : 'asc';
return $query->orderBy(ltrim($sort, '-'), $direction);
}, function ($query) {
return $query->latest();
})
}, function ($query) {
return $query->latest();
})
->when($request->input('filter.active'), function ($query, $value) {
$active = $value === 'true' ? true : false;

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePersonalAccessTokenRequest;
use App\Http\Resources\PersonalAccessTokenResource;
class PersonalAccessTokenController extends Controller
{
public function index()
{
return PersonalAccessTokenResource::collection(user()->tokens);
}
public function store(StorePersonalAccessTokenRequest $request)
{
$token = user()->createToken($request->name);
return [
'token' => new PersonalAccessTokenResource($token->accessToken),
'accessToken' => explode('|', $token->plainTextToken, 2)[1]
];
}
public function destroy($id)
{
$token = user()->tokens()->findOrFail($id);
$token->delete();
return response('', 204);
}
}

View File

@ -10,7 +10,7 @@ class BrowserSessionController extends Controller
public function destroy(Request $request)
{
$request->validate([
'current_password_sesssions' => 'password',
'current_password_sesssions' => 'current_password',
]);
Auth::logoutOtherDevices($request->current_password_sesssions);

View File

@ -37,10 +37,10 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePersonalAccessTokenRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => [
'required',
'string',
'max:50'
]
];
}
}

View File

@ -26,7 +26,7 @@ class AliasResource extends JsonResource
'recipients' => RecipientResource::collection($this->whenLoaded('recipients')),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
'deleted_at' => $this->deleted_at ? $this->deleted_at->toDateTimeString() : null,
'deleted_at' => $this->deleted_at?->toDateTimeString(),
];
}
}

View File

@ -17,9 +17,9 @@ class DomainResource extends JsonResource
'default_recipient' => new RecipientResource($this->whenLoaded('defaultRecipient')),
'active' => $this->active,
'catch_all' => $this->catch_all,
'domain_verified_at' => $this->domain_verified_at ? $this->domain_verified_at->toDateTimeString() : null,
'domain_mx_validated_at' => $this->domain_mx_validated_at ? $this->domain_mx_validated_at->toDateTimeString() : null,
'domain_sending_verified_at' => $this->domain_sending_verified_at ? $this->domain_sending_verified_at->toDateTimeString() : null,
'domain_verified_at' => $this->domain_verified_at?->toDateTimeString(),
'domain_mx_validated_at' => $this->domain_mx_validated_at?->toDateTimeString(),
'domain_sending_verified_at' => $this->domain_sending_verified_at?->toDateTimeString(),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
];

View File

@ -21,7 +21,7 @@ class FailedDeliveryResource extends JsonResource
'email_type' => $this->email_type,
'status' => $this->status,
'code' => $this->code,
'attempted_at' => $this->attempted_at ? $this->attempted_at->toDateTimeString() : null,
'attempted_at' => $this->attempted_at?->toDateTimeString(),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
];

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PersonalAccessTokenResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'user_id' => $this->tokenable_id,
'name' => $this->name,
'abilities' => $this->abilities,
'last_used_at' => $this->last_used_at?->toDateTimeString(),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),
];
}
}

View File

@ -15,7 +15,7 @@ class RecipientResource extends JsonResource
'can_reply_send' => $this->can_reply_send,
'should_encrypt' => $this->should_encrypt,
'fingerprint' => $this->fingerprint,
'email_verified_at' => $this->email_verified_at ? $this->email_verified_at->toDateTimeString() : null,
'email_verified_at' => $this->email_verified_at?->toDateTimeString(),
'aliases' => AliasResource::collection($this->whenLoaded('aliases')),
'created_at' => $this->created_at->toDateTimeString(),
'updated_at' => $this->updated_at->toDateTimeString(),

View File

@ -43,7 +43,10 @@ class EmailData
$this->encryptedParts = $parser->getAttachments();
} else {
foreach ($parser->getAttachments() as $attachment) {
if ($attachment->getContentDisposition() === 'inline') {
// Incorrect content type "text", set as text/plain
if ($attachment->getContentType() === 'text') {
$this->text = base64_encode(stream_get_contents($attachment->getStream()));
} elseif ($attachment->getContentDisposition() === 'inline') {
$this->inlineAttachments[] = [
'stream' => base64_encode(stream_get_contents($attachment->getStream())),
'file_name' => base64_encode($attachment->getFileName()),

View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use App\Traits\HasUuid;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
use HasFactory;
use HasUuid;
public $incrementing = false;
protected $keyType = 'string';
}

View File

@ -12,7 +12,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements MustVerifyEmail
{

View File

@ -2,12 +2,14 @@
namespace App\Providers;
use App\Models\PersonalAccessToken;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
@ -18,7 +20,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
Sanctum::ignoreMigrations();
}
/**
@ -30,6 +32,8 @@ class AppServiceProvider extends ServiceProvider
{
Blade::withoutComponentTags();
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Builder::macro('jsonPaginate', function (int $maxResults = null, int $defaultSize = null) {
$maxResults = $maxResults ?? 100;
$defaultSize = $defaultSize ?? 100;

View File

@ -3,7 +3,6 @@
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider
{
@ -24,13 +23,5 @@ class AuthServiceProvider extends ServiceProvider
public function boot()
{
$this->registerPolicies();
Passport::routes(function ($router) {
$router->forPersonalAccessTokens();
}, ['middleware' => ['web', 'auth', '2fa']]);
Passport::cookie('anonaddy_token');
Passport::personalAccessTokensExpireIn(now()->addYears(5));
}
}

View File

@ -13,7 +13,7 @@
"doctrine/dbal": "^3.0",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^9.11",
"laravel/passport": "^10.0",
"laravel/sanctum": "^2.15",
"laravel/tinker": "^2.7",
"laravel/ui": "^3.0",
"maatwebsite/excel": "^3.1",
@ -28,7 +28,8 @@
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^6.1",
"phpunit/phpunit": "^9.5.10",
"spatie/laravel-ignition": "^1.0"
"spatie/laravel-ignition": "^1.0",
"spatie/laravel-ray": "^1.29"
},
"config": {
"optimize-autoloader": true,

1569
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,11 +40,6 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
/*

67
config/sanctum.php Normal file
View File

@ -0,0 +1,67 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];

View File

@ -0,0 +1,91 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePersonalAccessTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->uuid('id');
$table->uuidMorphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
$table->primary('id');
});
// Drop Laravel Passport tables
Schema::dropIfExists('oauth_auth_codes');
Schema::dropIfExists('oauth_access_tokens');
Schema::dropIfExists('oauth_refresh_tokens');
Schema::dropIfExists('oauth_clients');
Schema::dropIfExists('oauth_personal_access_clients');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('personal_access_tokens');
// Re-create Laravel Passport tables
Schema::create('oauth_auth_codes', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->uuid('user_id');
$table->unsignedInteger('client_id');
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
Schema::create('oauth_access_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->uuid('user_id')->index()->nullable();
$table->unsignedInteger('client_id');
$table->string('name')->nullable();
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->timestamps();
$table->dateTime('expires_at')->nullable();
});
Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->string('access_token_id', 100)->index();
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
Schema::create('oauth_clients', function (Blueprint $table) {
$table->increments('id');
$table->uuid('user_id')->index()->nullable();
$table->string('name');
$table->string('secret', 100);
$table->text('redirect');
$table->boolean('personal_access_client');
$table->boolean('password_client');
$table->boolean('revoked');
$table->timestamps();
});
Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('client_id')->index();
$table->timestamps();
});
}
}

967
package-lock.json generated

File diff suppressed because it is too large Load Diff

4
resources/js/app.js vendored
View File

@ -33,8 +33,8 @@ Vue.component('rules', require('./pages/Rules.vue').default)
Vue.component('failed-deliveries', require('./pages/FailedDeliveries.vue').default)
Vue.component(
'passport-personal-access-tokens',
require('./components/passport/PersonalAccessTokens.vue').default
'personal-access-tokens',
require('./components/sanctum/PersonalAccessTokens.vue').default
)
Vue.component('webauthn-keys', require('./components/WebauthnKeys.vue').default)

View File

@ -29,16 +29,17 @@
class="text-indigo-700"
>Chrome / Brave</a
>
to generate new aliases. Simply paste the token generated below into the browser extension to
get started. Your API access tokens are secret and should be treated like your password. For
more information please see the <a href="/docs" class="text-indigo-700">API documentation</a>.
to create new aliases. They can also be used with the mobile apps. Simply paste a token you've
created into the browser extension or mobile apps to get started. Your API access tokens are
secret and should be treated like your password. For more information please see the
<a href="/docs" class="text-indigo-700">API documentation</a>.
</p>
<button
@click="openCreateTokenModal"
class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
>
Generate New Token
Create New Token
</button>
<div class="mt-6">
@ -47,8 +48,8 @@
<div class="my-4 w-24 border-b-2 border-grey-200"></div>
<p class="my-6">
Tokens you have generated that can be used to access the API. To revoke an access token
simply click the delete button next to it.
Tokens you have created that can be used to access the API. To revoke an access token simply
click the delete button next to it.
</p>
<div>
@ -60,7 +61,7 @@
<div class="table-row">
<div class="table-cell p-1 md:p-4 font-semibold">Name</div>
<div class="table-cell p-1 md:p-4 font-semibold">Created</div>
<div class="table-cell p-1 md:p-4 font-semibold">Expires</div>
<div class="table-cell p-1 md:p-4 font-semibold">Last Used</div>
<div class="table-cell p-1 md:p-4"></div>
</div>
<div
@ -70,7 +71,10 @@
>
<div class="table-cell p-1 md:p-4">{{ token.name }}</div>
<div class="table-cell p-1 md:p-4">{{ token.created_at | timeAgo }}</div>
<div class="table-cell p-1 md:p-4">{{ token.expires_at | timeAgo }}</div>
<div v-if="token.last_used_at" class="table-cell p-1 md:p-4">
{{ token.last_used_at | timeAgo }}
</div>
<div v-else class="table-cell p-1 md:p-4">Not used yet</div>
<div class="table-cell p-1 md:p-4 text-right">
<a
class="text-red-500 font-bold cursor-pointer focus:outline-none"
@ -118,7 +122,7 @@
:class="loading ? 'cursor-not-allowed' : ''"
:disabled="loading"
>
Generate Token
Create Token
<loader v-if="loading" />
</button>
<button
@ -141,8 +145,10 @@
</p>
<textarea
v-model="accessToken"
class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 text-sm"
rows="10"
@click="selectTokenTextArea"
id="token-text-area"
class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 text-md break-all"
rows="1"
readonly
>
</textarea>
@ -226,8 +232,8 @@ export default {
methods: {
getTokens() {
axios.get('/oauth/personal-access-tokens').then(response => {
this.tokens = response.data
axios.get('/settings/personal-access-tokens').then(response => {
this.tokens = response.data.data
})
},
store() {
@ -236,7 +242,7 @@ export default {
this.form.errors = []
axios
.post('/oauth/personal-access-tokens', this.form)
.post('/settings/personal-access-tokens', this.form)
.then(response => {
this.loading = false
this.form.name = ''
@ -261,12 +267,19 @@ export default {
revoke() {
this.revokeTokenLoading = true
axios.delete(`/oauth/personal-access-tokens/${this.tokenToRevoke.id}`).then(response => {
this.revokeTokenLoading = false
this.revokeTokenModalOpen = false
this.tokenToRevoke = null
this.getTokens()
})
axios
.delete(`/settings/personal-access-tokens/${this.tokenToRevoke.id}`)
.then(response => {
this.revokeTokenLoading = false
this.revokeTokenModalOpen = false
this.tokenToRevoke = null
this.getTokens()
})
.catch(error => {
this.revokeTokenLoading = false
this.revokeTokenModalOpen = false
this.error()
})
},
openCreateTokenModal() {
this.accessToken = null
@ -278,6 +291,11 @@ export default {
closeRevokeTokenModal() {
this.revokeTokenModalOpen = false
},
selectTokenTextArea() {
let textArea = document.getElementById('token-text-area')
textArea.focus()
textArea.select()
},
clipboardSuccess() {
this.success('Copied to clipboard')
},

View File

@ -1,13 +0,0 @@
@component('mail::message')
# Your API token expires soon
One of the API tokens on your AnonAddy account will expire in **one weeks time**.</p>
If you are not using this API token for the browser extension or to access the API then you do not need to take any action.
If you **are using the token** for the browser extension please log into your account and generate a new API token and add this to the browser extension before your current one expires.
Once an API token has expired it can no longer be used to access the API.
@endcomponent

View File

@ -679,7 +679,7 @@
<div class="px-6 py-8 md:p-10 bg-white rounded-lg shadow mb-10">
<passport-personal-access-tokens />
<personal-access-tokens />
</div>

View File

@ -36,7 +36,7 @@ use Illuminate\Support\Facades\Route;
*/
Route::group([
'middleware' => ['auth:api', 'verified'],
'middleware' => ['auth:sanctum', 'verified'],
'prefix' => 'v1'
], function () {
Route::controller(AliasController::class)->group(function () {

View File

@ -3,6 +3,7 @@
use App\Http\Controllers\AliasExportController;
use App\Http\Controllers\Auth\BackupCodeController;
use App\Http\Controllers\Auth\ForgotUsernameController;
use App\Http\Controllers\Auth\PersonalAccessTokenController;
use App\Http\Controllers\Auth\TwoFactorAuthController;
use App\Http\Controllers\Auth\WebauthnController;
use App\Http\Controllers\Auth\WebauthnEnabledKeyController;
@ -129,5 +130,11 @@ Route::group([
Route::post('/2fa/new-backup-code', [BackupCodeController::class, 'update'])->name('settings.new_backup_code');
Route::controller(PersonalAccessTokenController::class)->group(function () {
Route::get('/personal-access-tokens', 'index')->name('personal_access_tokens.index');
Route::post('/personal-access-tokens', 'store')->name('personal_access_tokens.store');
Route::delete('/personal-access-tokens/{id}', 'destroy')->name('personal_access_tokens.destroy');
});
Route::get('/aliases/export', [AliasExportController::class, 'export'])->name('aliases.export');
});

View File

@ -12,7 +12,7 @@ class AccountDetailsTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
}
/** @test */

View File

@ -15,7 +15,7 @@ class AliasRecipientsTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
}
/** @test */

View File

@ -16,7 +16,7 @@ class AliasesTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
$this->user->recipients()->save($this->user->defaultRecipient);
$this->user->usernames()->save($this->user->defaultUsername);

View File

@ -4,9 +4,7 @@ namespace Tests\Feature\Api;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\Passport;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ApiTokensTest extends TestCase
@ -20,60 +18,40 @@ class ApiTokensTest extends TestCase
parent::setUp();
$this->user = User::factory()->create();
Passport::actingAs($this->user, [], 'web');
Sanctum::actingAs($this->user, [], 'web');
$this->user->recipients()->save($this->user->defaultRecipient);
$clientRepository = new ClientRepository();
$client = $clientRepository->createPersonalAccessClient(
null,
'Test Personal Access Client',
config('app.url')
);
DB::table('oauth_personal_access_clients')->insert([
'client_id' => $client->id,
'created_at' => now(),
'updated_at' => now(),
]);
}
/** @test */
public function user_can_generate_api_token()
public function user_can_create_api_token()
{
$response = $this->post('/oauth/personal-access-tokens', [
$response = $this->post('/settings/personal-access-tokens', [
'name' => 'New'
]);
$response->assertStatus(200);
$this->assertNotNull($response->getData()->accessToken);
$this->assertDatabaseHas('oauth_access_tokens', [
$this->assertDatabaseHas('personal_access_tokens', [
'name' => 'New',
'user_id' => $this->user->id
'tokenable_id' => $this->user->id
]);
}
/** @test */
public function user_can_revoke_api_token()
{
DB::table('oauth_access_tokens')->insert([
'id' => '1830c31e8e17dc4e871aa21ebe82e6cbfdd0d5781bec42631dd381119f355a911075f7e1a3dc2240',
'name' => 'New',
'user_id' => $this->user->id,
'revoked' => false,
'client_id' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$token = $this->user->createToken('New');
$response = $this->delete('/oauth/personal-access-tokens/1830c31e8e17dc4e871aa21ebe82e6cbfdd0d5781bec42631dd381119f355a911075f7e1a3dc2240');
$response = $this->delete("/settings/personal-access-tokens/{$token->accessToken->id}");
$response->assertStatus(204);
$this->assertDatabaseMissing('oauth_access_tokens', [
$this->assertEmpty($this->user->tokens);
$this->assertDatabaseMissing('personal_access_tokens', [
'name' => 'New',
'user_id' => $this->user->id,
'id' => '1830c31e8e17dc4e871aa21ebe82e6cbfdd0d5781bec42631dd381119f355a911075f7e1a3dc2240',
'revoke' => true
'tokenable_id' => $this->user->id,
'id' => $token->accessToken->id
]);
}
}

View File

@ -13,7 +13,7 @@ class AppVersionTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
}
/** @test */

View File

@ -14,7 +14,7 @@ class DomainsTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
}
/** @test */

View File

@ -13,7 +13,7 @@ class FailedDeliveriesTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
$this->user->recipients()->save($this->user->defaultRecipient);
}

View File

@ -14,7 +14,7 @@ class RecipientsTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
}
/** @test */

View File

@ -18,7 +18,7 @@ class RulesTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
$this->user->recipients()->save($this->user->defaultRecipient);
$this->user->usernames()->save($this->user->defaultUsername);

View File

@ -16,7 +16,7 @@ class UsernamesTest extends TestCase
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
parent::setUpSanctum();
$this->user->recipients()->save($this->user->defaultRecipient);
$this->user->usernames()->save($this->user->defaultUsername);

View File

@ -5,10 +5,8 @@ namespace Tests;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\DB;
use Illuminate\Testing\TestResponse;
use Laravel\Passport\ClientRepository;
use Laravel\Passport\Passport;
use Laravel\Sanctum\Sanctum;
use PHPUnit\Framework\Assert;
abstract class TestCase extends BaseTestCase
@ -41,31 +39,9 @@ abstract class TestCase extends BaseTestCase
});
}
protected function setUpPassport(): void
protected function setUpSanctum(): void
{
$this->user = User::factory()->create();
Passport::actingAs($this->user, []);
$clientRepository = new ClientRepository();
$client = $clientRepository->createPersonalAccessClient(
null,
'Test Personal Access Client',
config('app.url')
);
DB::table('oauth_personal_access_clients')->insert([
'client_id' => $client->id,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('oauth_access_tokens')->insert([
'id' => '1830c31e8e17dc4e871aa21ebe82e6cbfdd0d5781bec42631dd381119f355a911075f7e1a3dc2240',
'name' => 'New',
'user_id' => $this->user->id,
'revoked' => false,
'client_id' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
Sanctum::actingAs($this->user, []);
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace Tests\Unit;
use App\Mail\TokenExpiringSoon;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class EmailUsersWithTokenExpiringSoonTest extends TestCase
{
use RefreshDatabase;
protected $user;
protected function setUp(): void
{
parent::setUp();
parent::setUpPassport();
$this->user->tokens()->first()->update(['expires_at' => Carbon::create(2019, 1, 31)]);
Mail::fake();
}
/** @test */
public function it_can_send_a_mail_concerning_a_token_expiring_soon()
{
$this->setNow(2019, 1, 28);
$this->artisan('anonaddy:email-users-with-token-expiring-soon');
Mail::assertNotQueued(TokenExpiringSoon::class);
$this->setNow(2019, 1, 29);
$this->artisan('anonaddy:email-users-with-token-expiring-soon');
Mail::assertNotQueued(TokenExpiringSoon::class);
$this->setNow(2019, 1, 24);
$this->artisan('anonaddy:email-users-with-token-expiring-soon');
Mail::assertQueued(TokenExpiringSoon::class, 1);
Mail::assertQueued(TokenExpiringSoon::class, function (TokenExpiringSoon $mail) {
return $mail->hasTo($this->user->email);
});
}
/** @test */
public function it_does_not_send_a_mail_for_revoked_tokens()
{
$this->user->tokens()->first()->revoke();
$this->setNow(2019, 1, 24);
$this->artisan('anonaddy:email-users-with-token-expiring-soon');
Mail::assertNotQueued(TokenExpiringSoon::class);
}
protected function setNow(int $year, int $month, int $day)
{
$newNow = Carbon::create($year, $month, $day)->startOfDay();
Carbon::setTestNow($newNow);
return $this;
}
}