mirror of
https://github.com/anonaddy/anonaddy.git
synced 2025-12-28 07:55:07 +00:00
Migrate from Passport to Sanctum for API
This commit is contained in:
parent
d7b40a9259
commit
5bfc5e5e9f
@ -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
1
.gitignore
vendored
@ -17,3 +17,4 @@ yarn-error.log
|
||||
.env
|
||||
.php-cs-fixer.cache
|
||||
.phpunit.result.cache
|
||||
ray.php
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
34
app/Http/Controllers/Auth/PersonalAccessTokenController.php
Normal file
34
app/Http/Controllers/Auth/PersonalAccessTokenController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
],
|
||||
|
||||
34
app/Http/Requests/StorePersonalAccessTokenRequest.php
Normal file
34
app/Http/Requests/StorePersonalAccessTokenRequest.php
Normal 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'
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
|
||||
21
app/Http/Resources/PersonalAccessTokenResource.php
Normal file
21
app/Http/Resources/PersonalAccessTokenResource.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
@ -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()),
|
||||
|
||||
17
app/Models/PersonalAccessToken.php
Normal file
17
app/Models/PersonalAccessToken.php
Normal 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';
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
1569
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -40,11 +40,6 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'passport',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
67
config/sanctum.php
Normal file
67
config/sanctum.php
Normal 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,
|
||||
],
|
||||
|
||||
];
|
||||
@ -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
967
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
4
resources/js/app.js
vendored
4
resources/js/app.js
vendored
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
},
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@ class AccountDetailsTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUpPassport();
|
||||
parent::setUpSanctum();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@ -15,7 +15,7 @@ class AliasRecipientsTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUpPassport();
|
||||
parent::setUpSanctum();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ class AppVersionTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUpPassport();
|
||||
parent::setUpSanctum();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@ -14,7 +14,7 @@ class DomainsTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUpPassport();
|
||||
parent::setUpSanctum();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ class RecipientsTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
parent::setUpPassport();
|
||||
parent::setUpSanctum();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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, []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user