浏览代码

Updated to vue 3

Will Browning 2 年之前
父节点
当前提交
150542f3c1
共有 43 个文件被更改,包括 1550 次插入1261 次删除
  1. 1 1
      README.md
  2. 0 3
      app/Console/Commands/ReceiveEmail.php
  3. 24 0
      app/Http/Controllers/Api/ApiTokenDetailController.php
  4. 2 0
      app/Http/Controllers/Api/RecipientKeyController.php
  5. 100 0
      app/Http/Controllers/Auth/ApiAuthenticationController.php
  6. 1 1
      app/Http/Controllers/Auth/ForgotUsernameController.php
  7. 4 1
      app/Http/Controllers/Auth/PersonalAccessTokenController.php
  8. 1 1
      app/Http/Kernel.php
  9. 1 1
      app/Http/Middleware/VerifyCsrfToken.php
  10. 32 0
      app/Http/Requests/ApiAuthenticationLoginRequest.php
  11. 32 0
      app/Http/Requests/ApiAuthenticationMfaRequest.php
  12. 1 1
      app/Traits/CheckUserRules.php
  13. 1 0
      composer.json
  14. 203 344
      composer.lock
  15. 1 1
      config/google2fa.php
  16. 404 211
      package-lock.json
  17. 11 11
      package.json
  18. 1 1
      phpunit.xml
  19. 15 8
      resources/css/app.css
  20. 44 45
      resources/js/app.js
  21. 55 137
      resources/js/components/Modal.vue
  22. 24 54
      resources/js/components/MoreOptions.vue
  23. 27 15
      resources/js/components/Toggle.vue
  24. 4 8
      resources/js/components/WebauthnKeys.vue
  25. 115 114
      resources/js/components/sanctum/PersonalAccessTokens.vue
  26. 64 82
      resources/js/pages/Aliases.vue
  27. 42 53
      resources/js/pages/Domains.vue
  28. 13 17
      resources/js/pages/FailedDeliveries.vue
  29. 25 40
      resources/js/pages/Recipients.vue
  30. 29 51
      resources/js/pages/Rules.vue
  31. 42 50
      resources/js/pages/Usernames.vue
  32. 5 0
      resources/views/auth/login.blade.php
  33. 1 1
      resources/views/domains/index.blade.php
  34. 0 1
      resources/views/layouts/app.blade.php
  35. 1 1
      resources/views/usernames/index.blade.php
  36. 3 0
      routes/api.php
  37. 5 1
      routes/web.php
  38. 0 4
      tests/Feature/Api/AliasesTest.php
  39. 34 0
      tests/Feature/Api/ApiTokenDetailsTest.php
  40. 178 0
      tests/Feature/ApiAuthenticationTest.php
  41. 4 0
      tests/Feature/LoginTest.php
  42. 0 1
      tests/Feature/ShowFailedDeliveriesTest.php
  43. 0 1
      tests/Feature/ShowRecipientsTest.php

+ 1 - 1
README.md

@@ -462,7 +462,7 @@ For full details please see the [self-hosting instructions file](SELF-HOSTING.md
 
 
 ## My sponsors
 ## My sponsors
 
 
-Thanks to [Vlad Timofeev](https://github.com/vlad-timofeev), [Patrick Dobler](https://github.com/patrickdobler) and [Luca Steeb](https://github.com/steebchen) for supporting me by sponsoring the project on GitHub!
+Thanks to [Vlad Timofeev](https://github.com/vlad-timofeev), [Patrick Dobler](https://github.com/patrickdobler), [Luca Steeb](https://github.com/steebchen) and [Laiteux](https://github.com/Laiteux) for supporting me by sponsoring the project on GitHub!
 
 
 Also an extra special thanks to [CrazyMax](https://github.com/crazy-max) for sponsoring me and also creating and maintaining the awesome [AnonAddy Docker image](https://github.com/anonaddy/docker)!
 Also an extra special thanks to [CrazyMax](https://github.com/crazy-max) for sponsoring me and also creating and maintaining the awesome [AnonAddy Docker image](https://github.com/anonaddy/docker)!
 
 

+ 0 - 3
app/Console/Commands/ReceiveEmail.php

@@ -81,7 +81,6 @@ class ReceiveEmail extends Command
             $this->size = $this->option('size') / ($recipientCount ? $recipientCount : 1);
             $this->size = $this->option('size') / ($recipientCount ? $recipientCount : 1);
 
 
             foreach ($recipients as $recipient) {
             foreach ($recipients as $recipient) {
-
                 // Handle bounces
                 // Handle bounces
                 if ($this->option('sender') === 'MAILER-DAEMON') {
                 if ($this->option('sender') === 'MAILER-DAEMON') {
                     $this->handleBounce($recipient['email']);
                     $this->handleBounce($recipient['email']);
@@ -145,7 +144,6 @@ class ReceiveEmail extends Command
                 $verifiedRecipient = $user->getVerifiedRecipientByEmail($this->senderFrom);
                 $verifiedRecipient = $user->getVerifiedRecipientByEmail($this->senderFrom);
 
 
                 if ($validEmailDestination && $verifiedRecipient?->can_reply_send) {
                 if ($validEmailDestination && $verifiedRecipient?->can_reply_send) {
-
                     // Check if the Dmarc allow or spam headers are present from Rspamd
                     // Check if the Dmarc allow or spam headers are present from Rspamd
                     if (! $this->parser->getHeader('X-AnonAddy-Dmarc-Allow') || $this->parser->getHeader('X-AnonAddy-Spam')) {
                     if (! $this->parser->getHeader('X-AnonAddy-Dmarc-Allow') || $this->parser->getHeader('X-AnonAddy-Spam')) {
                         // Notify user and exit
                         // Notify user and exit
@@ -295,7 +293,6 @@ class ReceiveEmail extends Command
 
 
             // Verify queue ID
             // Verify queue ID
             if (isset($dsn['X-postfix-queue-id'])) {
             if (isset($dsn['X-postfix-queue-id'])) {
-
                 // First check in DB
                 // First check in DB
                 $postfixQueueId = PostfixQueueId::firstWhere('queue_id', strtoupper($dsn['X-postfix-queue-id']));
                 $postfixQueueId = PostfixQueueId::firstWhere('queue_id', strtoupper($dsn['X-postfix-queue-id']));
 
 

+ 24 - 0
app/Http/Controllers/Api/ApiTokenDetailController.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+class ApiTokenDetailController extends Controller
+{
+    public function show(Request $request)
+    {
+        $token = $request->user()->currentAccessToken();
+
+        if (! $token) {
+            return response('Current token could not be found', 404);
+        }
+
+        return response()->json([
+            'name' => $token->name,
+            'created_at' => $token->created_at?->toDateTimeString(),
+            'expires_at' => $token->expires_at?->toDateTimeString()
+        ]);
+    }
+}

+ 2 - 0
app/Http/Controllers/Api/RecipientKeyController.php

@@ -43,6 +43,8 @@ class RecipientKeyController extends Controller
             'should_encrypt' => false,
             'should_encrypt' => false,
             'inline_encryption' => false,
             'inline_encryption' => false,
             'protected_headers' => false,
             'protected_headers' => false,
+            'inline_encryption' => false,
+            'protected_headers' => false,
             'fingerprint' => null
             'fingerprint' => null
         ]);
         ]);
 
 

+ 100 - 0
app/Http/Controllers/Auth/ApiAuthenticationController.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace App\Http\Controllers\Auth;
+
+use App\Facades\Webauthn;
+use App\Http\Controllers\Controller;
+use App\Http\Requests\ApiAuthenticationLoginRequest;
+use App\Http\Requests\ApiAuthenticationMfaRequest;
+use App\Models\User;
+use App\Models\Username;
+use Illuminate\Contracts\Encryption\DecryptException;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\Hash;
+use PragmaRX\Google2FA\Google2FA;
+
+class ApiAuthenticationController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('throttle:3,1');
+    }
+
+    public function login(ApiAuthenticationLoginRequest $request)
+    {
+        $user = Username::firstWhere('username', $request->username)?->user;
+
+        if (! $user || ! Hash::check($request->password, $user->password)) {
+            return response()->json([
+                'error' => 'The provided credentials are incorrect'
+            ], 401);
+        }
+
+        // Check if user has 2FA enabled, if needs OTP then return mfa_key
+        if ($user->two_factor_enabled) {
+            return response()->json([
+                'message' => "OTP required, please make a request to /api/auth/mfa with the 'mfa_key', 'otp' and 'device_name' including a 'X-CSRF-TOKEN' header",
+                'mfa_key' => Crypt::encryptString($user->id.'|'.config('anonaddy.secret').'|'.Carbon::now()->addMinutes(5)->getTimestamp()),
+                'csrf_token' => csrf_token()
+            ], 422);
+        } elseif (Webauthn::enabled($user)) {
+            // If WebAuthn is enabled then return currently unsupported message
+            return response()->json([
+                'error' => 'WebAuthn authentication is not currently supported from the extension or mobile apps, please use an API key to login instead'
+            ], 403);
+        }
+
+        // If the user doesn't use 2FA then return the new API key
+        return response()->json([
+            'api_key' => explode('|', $user->createToken($request->device_name)->plainTextToken, 2)[1]
+        ]);
+    }
+
+    public function mfa(ApiAuthenticationMfaRequest $request)
+    {
+        try {
+            $mfaKey = Crypt::decryptString($request->mfa_key);
+        } catch (DecryptException $e) {
+            return response()->json([
+                'error' => 'Invalid mfa_key'
+            ], 401);
+        }
+        $parts = explode('|', $mfaKey, 3);
+
+        $user = User::find($parts[0]);
+
+        if (! $user || $parts[1] !== config('anonaddy.secret')) {
+            return response()->json([
+                'error' => 'Invalid mfa_key'
+            ], 401);
+        }
+
+        // Check if the mfa_key has expired
+        if (Carbon::now()->getTimestamp() > $parts[2]) {
+            return response()->json([
+                'error' => 'mfa_key expired, please request a new one at /api/auth/login'
+            ], 401);
+        }
+
+        $google2fa = new Google2FA();
+        $lastTimeStamp = Cache::get('2fa_ts:'.$user->id);
+
+        $timestamp = $google2fa->verifyKeyNewer($user->two_factor_secret, $request->otp, $lastTimeStamp);
+
+        if (! $timestamp) {
+            return response()->json([
+                'error' => 'The \'One Time Password\' typed was wrong'
+            ], 401);
+        }
+
+        if (is_int($timestamp)) {
+            Cache::put('2fa_ts:'.$user->id, $timestamp, now()->addMinutes(5));
+        }
+
+        return response()->json([
+            'api_key' => explode('|', $user->createToken($request->device_name)->plainTextToken, 2)[1]
+        ]);
+    }
+}

+ 1 - 1
app/Http/Controllers/Auth/ForgotUsernameController.php

@@ -39,7 +39,7 @@ class ForgotUsernameController extends Controller
     {
     {
         $this->validateEmail($request);
         $this->validateEmail($request);
 
 
-        $recipient = Recipient::select(['id', 'email', 'email_verified_at'])->whereNotNull('email_verified_at')->get()->firstWhere('email', strtolower($request->email));
+        $recipient = Recipient::select(['id', 'user_id', 'email', 'should_encrypt', 'fingerprint', 'email_verified_at'])->whereNotNull('email_verified_at')->get()->firstWhere('email', strtolower($request->email));
 
 
         if (isset($recipient)) {
         if (isset($recipient)) {
             $recipient->sendUsernameReminderNotification();
             $recipient->sendUsernameReminderNotification();

+ 4 - 1
app/Http/Controllers/Auth/PersonalAccessTokenController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use App\Http\Requests\StorePersonalAccessTokenRequest;
 use App\Http\Requests\StorePersonalAccessTokenRequest;
 use App\Http\Resources\PersonalAccessTokenResource;
 use App\Http\Resources\PersonalAccessTokenResource;
+use chillerlan\QRCode\QRCode;
 
 
 class PersonalAccessTokenController extends Controller
 class PersonalAccessTokenController extends Controller
 {
 {
@@ -24,10 +25,12 @@ class PersonalAccessTokenController extends Controller
         }
         }
 
 
         $token = user()->createToken($request->name, ['*'], $expiration);
         $token = user()->createToken($request->name, ['*'], $expiration);
+        $accessToken = explode('|', $token->plainTextToken, 2)[1];
 
 
         return [
         return [
             'token' => new PersonalAccessTokenResource($token->accessToken),
             'token' => new PersonalAccessTokenResource($token->accessToken),
-            'accessToken' => explode('|', $token->plainTextToken, 2)[1]
+            'accessToken' => $accessToken,
+            'qrCode' => (new QRCode())->render(config('app.url') . "|" . $accessToken)
         ];
         ];
     }
     }
 
 

+ 1 - 1
app/Http/Kernel.php

@@ -62,7 +62,7 @@ class Kernel extends HttpKernel
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
         'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
         'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
         'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
         'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
-        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
         '2fa' => \App\Http\Middleware\VerifyTwoFactorAuth::class,
         '2fa' => \App\Http\Middleware\VerifyTwoFactorAuth::class,
         'webauthn' => \App\Http\Middleware\VerifyWebauthn::class,
         'webauthn' => \App\Http\Middleware\VerifyWebauthn::class,

+ 1 - 1
app/Http/Middleware/VerifyCsrfToken.php

@@ -19,6 +19,6 @@ class VerifyCsrfToken extends Middleware
      * @var array
      * @var array
      */
      */
     protected $except = [
     protected $except = [
-        //
+        'api/auth/login'
     ];
     ];
 }
 }

+ 32 - 0
app/Http/Requests/ApiAuthenticationLoginRequest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ApiAuthenticationLoginRequest 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 [
+            'username' => 'required|string',
+            'password' => 'required|string',
+            'device_name' => 'required|string|max:50'
+        ];
+    }
+}

+ 32 - 0
app/Http/Requests/ApiAuthenticationMfaRequest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ApiAuthenticationMfaRequest 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 [
+            'mfa_key' => 'required|string|max:500',
+            'otp' => 'required|string|min:6|max:6',
+            'device_name' => 'required|string|max:50'
+        ];
+    }
+}

+ 1 - 1
app/Traits/CheckUserRules.php

@@ -101,7 +101,7 @@ trait CheckUserRules
                     return ! Str::endsWith($variable, $value);
                     return ! Str::endsWith($variable, $value);
                 });
                 });
                 break;
                 break;
-            // regex preg_match?
+                // regex preg_match?
         }
         }
     }
     }
 
 

+ 1 - 0
composer.json

@@ -10,6 +10,7 @@
         "php": "^8.0.2",
         "php": "^8.0.2",
         "asbiin/laravel-webauthn": "^3.0.0",
         "asbiin/laravel-webauthn": "^3.0.0",
         "bacon/bacon-qr-code": "^2.0",
         "bacon/bacon-qr-code": "^2.0",
+        "chillerlan/php-qrcode": "^4.3",
         "doctrine/dbal": "^3.0",
         "doctrine/dbal": "^3.0",
         "guzzlehttp/guzzle": "^7.2",
         "guzzlehttp/guzzle": "^7.2",
         "laravel/framework": "^9.11",
         "laravel/framework": "^9.11",

+ 203 - 344
composer.lock

@@ -4,20 +4,20 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "e82a971bfd5ab4ba0fa31965a1c6ca60",
+    "content-hash": "6a781a6eb7831ca8568ca458163ccb56",
     "packages": [
     "packages": [
         {
         {
             "name": "asbiin/laravel-webauthn",
             "name": "asbiin/laravel-webauthn",
-            "version": "3.2.0",
+            "version": "3.2.2",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/asbiin/laravel-webauthn.git",
                 "url": "https://github.com/asbiin/laravel-webauthn.git",
-                "reference": "747df5ab7bf0f88bbb36f11c0f281464d6c29d0b"
+                "reference": "05610549427974bf8a3f0cc32a58e4050d9f7792"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/747df5ab7bf0f88bbb36f11c0f281464d6c29d0b",
-                "reference": "747df5ab7bf0f88bbb36f11c0f281464d6c29d0b",
+                "url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/05610549427974bf8a3f0cc32a58e4050d9f7792",
+                "reference": "05610549427974bf8a3f0cc32a58e4050d9f7792",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -92,7 +92,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-07-30T09:08:07+00:00"
+            "time": "2022-08-20T17:56:48+00:00"
         },
         },
         {
         {
             "name": "bacon/bacon-qr-code",
             "name": "bacon/bacon-qr-code",
@@ -217,16 +217,16 @@
         },
         },
         {
         {
             "name": "brick/math",
             "name": "brick/math",
-            "version": "0.10.1",
+            "version": "0.10.2",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/brick/math.git",
                 "url": "https://github.com/brick/math.git",
-                "reference": "de846578401f4e58f911b3afeb62ced56365ed87"
+                "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/brick/math/zipball/de846578401f4e58f911b3afeb62ced56365ed87",
-                "reference": "de846578401f4e58f911b3afeb62ced56365ed87",
+                "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f",
+                "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -261,7 +261,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/brick/math/issues",
                 "issues": "https://github.com/brick/math/issues",
-                "source": "https://github.com/brick/math/tree/0.10.1"
+                "source": "https://github.com/brick/math/tree/0.10.2"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -269,7 +269,149 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-08-01T22:54:31+00:00"
+            "time": "2022-08-10T22:54:19+00:00"
+        },
+        {
+            "name": "chillerlan/php-qrcode",
+            "version": "4.3.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/chillerlan/php-qrcode.git",
+                "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d",
+                "reference": "2ca4bf5ae048af1981d1023ee42a0a2a9d51e51d",
+                "shasum": ""
+            },
+            "require": {
+                "chillerlan/php-settings-container": "^2.1.4",
+                "ext-mbstring": "*",
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "phan/phan": "^5.3",
+                "phpunit/phpunit": "^9.5",
+                "setasign/fpdf": "^1.8.2"
+            },
+            "suggest": {
+                "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.",
+                "setasign/fpdf": "Required to use the QR FPDF output."
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "chillerlan\\QRCode\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Kazuhiko Arase",
+                    "homepage": "https://github.com/kazuhikoarase"
+                },
+                {
+                    "name": "Smiley",
+                    "email": "smiley@chillerlan.net",
+                    "homepage": "https://github.com/codemasher"
+                },
+                {
+                    "name": "Contributors",
+                    "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors"
+                }
+            ],
+            "description": "A QR code generator. PHP 7.4+",
+            "homepage": "https://github.com/chillerlan/php-qrcode",
+            "keywords": [
+                "phpqrcode",
+                "qr",
+                "qr code",
+                "qrcode",
+                "qrcode-generator"
+            ],
+            "support": {
+                "issues": "https://github.com/chillerlan/php-qrcode/issues",
+                "source": "https://github.com/chillerlan/php-qrcode/tree/4.3.4"
+            },
+            "funding": [
+                {
+                    "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://ko-fi.com/codemasher",
+                    "type": "ko_fi"
+                }
+            ],
+            "time": "2022-07-25T09:12:45+00:00"
+        },
+        {
+            "name": "chillerlan/php-settings-container",
+            "version": "2.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/chillerlan/php-settings-container.git",
+                "reference": "1beb7df3c14346d4344b0b2e12f6f9a74feabd4a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/1beb7df3c14346d4344b0b2e12f6f9a74feabd4a",
+                "reference": "1beb7df3c14346d4344b0b2e12f6f9a74feabd4a",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "phan/phan": "^5.3",
+                "phpunit/phpunit": "^9.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "chillerlan\\Settings\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Smiley",
+                    "email": "smiley@chillerlan.net",
+                    "homepage": "https://github.com/codemasher"
+                }
+            ],
+            "description": "A container class for immutable settings objects. Not a DI container. PHP 7.4+",
+            "homepage": "https://github.com/chillerlan/php-settings-container",
+            "keywords": [
+                "PHP7",
+                "Settings",
+                "configuration",
+                "container",
+                "helper"
+            ],
+            "support": {
+                "issues": "https://github.com/chillerlan/php-settings-container/issues",
+                "source": "https://github.com/chillerlan/php-settings-container"
+            },
+            "funding": [
+                {
+                    "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://ko-fi.com/codemasher",
+                    "type": "ko_fi"
+                }
+            ],
+            "time": "2022-07-05T22:32:14+00:00"
         },
         },
         {
         {
             "name": "dasprid/enum",
             "name": "dasprid/enum",
@@ -488,16 +630,16 @@
         },
         },
         {
         {
             "name": "doctrine/dbal",
             "name": "doctrine/dbal",
-            "version": "3.4.0",
+            "version": "3.4.2",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "118a360e9437e88d49024f36283c8bcbd76105f5"
+                "reference": "22de295f10edbe00df74f517612f1fbd711131e2"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/118a360e9437e88d49024f36283c8bcbd76105f5",
-                "reference": "118a360e9437e88d49024f36283c8bcbd76105f5",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/22de295f10edbe00df74f517612f1fbd711131e2",
+                "reference": "22de295f10edbe00df74f517612f1fbd711131e2",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -579,7 +721,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.4.0"
+                "source": "https://github.com/doctrine/dbal/tree/3.4.2"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -595,7 +737,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2022-08-06T20:35:57+00:00"
+            "time": "2022-08-21T14:21:06+00:00"
         },
         },
         {
         {
             "name": "doctrine/deprecations",
             "name": "doctrine/deprecations",
@@ -1695,16 +1837,16 @@
         },
         },
         {
         {
             "name": "laravel/framework",
             "name": "laravel/framework",
-            "version": "v9.24.0",
+            "version": "v9.25.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "053840f579cf01d353d81333802afced79b1c0af"
+                "reference": "e8af8c2212e3717757ea7f459a655a2e9e771109"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/053840f579cf01d353d81333802afced79b1c0af",
-                "reference": "053840f579cf01d353d81333802afced79b1c0af",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/e8af8c2212e3717757ea7f459a655a2e9e771109",
+                "reference": "e8af8c2212e3717757ea7f459a655a2e9e771109",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -1871,7 +2013,7 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
                 "source": "https://github.com/laravel/framework"
             },
             },
-            "time": "2022-08-09T13:43:22+00:00"
+            "time": "2022-08-16T16:36:05+00:00"
         },
         },
         {
         {
             "name": "laravel/sanctum",
             "name": "laravel/sanctum",
@@ -2316,16 +2458,16 @@
         },
         },
         {
         {
             "name": "league/flysystem",
             "name": "league/flysystem",
-            "version": "3.2.0",
+            "version": "3.2.1",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a"
+                "reference": "81aea9e5217084c7850cd36e1587ee4aad721c6b"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/ed0ecc7f9b5c2f4a9872185846974a808a3b052a",
-                "reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/81aea9e5217084c7850cd36e1587ee4aad721c6b",
+                "reference": "81aea9e5217084c7850cd36e1587ee4aad721c6b",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -2386,7 +2528,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem/issues",
                 "issues": "https://github.com/thephpleague/flysystem/issues",
-                "source": "https://github.com/thephpleague/flysystem/tree/3.2.0"
+                "source": "https://github.com/thephpleague/flysystem/tree/3.2.1"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -2402,7 +2544,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2022-07-26T07:26:36+00:00"
+            "time": "2022-08-14T20:48:34+00:00"
         },
         },
         {
         {
             "name": "league/mime-type-detection",
             "name": "league/mime-type-detection",
@@ -7269,16 +7411,16 @@
         },
         },
         {
         {
             "name": "web-auth/cose-lib",
             "name": "web-auth/cose-lib",
-            "version": "v4.0.5",
+            "version": "v4.0.6",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/web-auth/cose-lib.git",
                 "url": "https://github.com/web-auth/cose-lib.git",
-                "reference": "2fe6c0d35136d75bc538372a317ca5df5a75ce73"
+                "reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/2fe6c0d35136d75bc538372a317ca5df5a75ce73",
-                "reference": "2fe6c0d35136d75bc538372a317ca5df5a75ce73",
+                "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/d72032843c97ba40edb682dfcec0b787b25cbe6f",
+                "reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -7338,7 +7480,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/web-auth/cose-lib/issues",
                 "issues": "https://github.com/web-auth/cose-lib/issues",
-                "source": "https://github.com/web-auth/cose-lib/tree/v4.0.5"
+                "source": "https://github.com/web-auth/cose-lib/tree/v4.0.6"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -7350,7 +7492,7 @@
                     "type": "patreon"
                     "type": "patreon"
                 }
                 }
             ],
             ],
-            "time": "2022-08-04T16:48:04+00:00"
+            "time": "2022-08-16T14:04:27+00:00"
         },
         },
         {
         {
             "name": "web-auth/metadata-service",
             "name": "web-auth/metadata-service",
@@ -8270,16 +8412,16 @@
         },
         },
         {
         {
             "name": "friendsofphp/php-cs-fixer",
             "name": "friendsofphp/php-cs-fixer",
-            "version": "v3.9.5",
+            "version": "v3.10.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
                 "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
-                "reference": "4465d70ba776806857a1ac2a6f877e582445ff36"
+                "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4465d70ba776806857a1ac2a6f877e582445ff36",
-                "reference": "4465d70ba776806857a1ac2a6f877e582445ff36",
+                "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe",
+                "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -8289,7 +8431,7 @@
                 "ext-json": "*",
                 "ext-json": "*",
                 "ext-tokenizer": "*",
                 "ext-tokenizer": "*",
                 "php": "^7.4 || ^8.0",
                 "php": "^7.4 || ^8.0",
-                "php-cs-fixer/diff": "^2.0",
+                "sebastian/diff": "^4.0",
                 "symfony/console": "^5.4 || ^6.0",
                 "symfony/console": "^5.4 || ^6.0",
                 "symfony/event-dispatcher": "^5.4 || ^6.0",
                 "symfony/event-dispatcher": "^5.4 || ^6.0",
                 "symfony/filesystem": "^5.4 || ^6.0",
                 "symfony/filesystem": "^5.4 || ^6.0",
@@ -8347,7 +8489,7 @@
             "description": "A tool to automatically fix PHP code style",
             "description": "A tool to automatically fix PHP code style",
             "support": {
             "support": {
                 "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
                 "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
-                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.9.5"
+                "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.10.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -8355,7 +8497,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-07-22T08:43:51+00:00"
+            "time": "2022-08-17T22:13:10+00:00"
         },
         },
         {
         {
             "name": "hamcrest/hamcrest-php",
             "name": "hamcrest/hamcrest-php",
@@ -8738,304 +8880,25 @@
             },
             },
             "time": "2022-02-21T01:04:05+00:00"
             "time": "2022-02-21T01:04:05+00:00"
         },
         },
-        {
-            "name": "php-cs-fixer/diff",
-            "version": "v2.0.2",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/PHP-CS-Fixer/diff.git",
-                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
-                "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^5.6 || ^7.0 || ^8.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
-                "symfony/process": "^3.3"
-            },
-            "type": "library",
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                }
-            ],
-            "description": "sebastian/diff v3 backport support for PHP 5.6+",
-            "homepage": "https://github.com/PHP-CS-Fixer",
-            "keywords": [
-                "diff"
-            ],
-            "support": {
-                "issues": "https://github.com/PHP-CS-Fixer/diff/issues",
-                "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
-            },
-            "time": "2020-10-14T08:32:19+00:00"
-        },
-        {
-            "name": "phpdocumentor/reflection-common",
-            "version": "2.2.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
-                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-2.x": "2.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Jaap van Otterdijk",
-                    "email": "opensource@ijaap.nl"
-                }
-            ],
-            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
-            "homepage": "http://www.phpdoc.org",
-            "keywords": [
-                "FQSEN",
-                "phpDocumentor",
-                "phpdoc",
-                "reflection",
-                "static analysis"
-            ],
-            "support": {
-                "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
-                "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
-            },
-            "time": "2020-06-27T09:03:43+00:00"
-        },
-        {
-            "name": "phpdocumentor/reflection-docblock",
-            "version": "5.3.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
-                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
-                "shasum": ""
-            },
-            "require": {
-                "ext-filter": "*",
-                "php": "^7.2 || ^8.0",
-                "phpdocumentor/reflection-common": "^2.2",
-                "phpdocumentor/type-resolver": "^1.3",
-                "webmozart/assert": "^1.9.1"
-            },
-            "require-dev": {
-                "mockery/mockery": "~1.3.2",
-                "psalm/phar": "^4.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                },
-                {
-                    "name": "Jaap van Otterdijk",
-                    "email": "account@ijaap.nl"
-                }
-            ],
-            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "support": {
-                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
-                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
-            },
-            "time": "2021-10-19T17:43:47+00:00"
-        },
-        {
-            "name": "phpdocumentor/type-resolver",
-            "version": "1.6.1",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "77a32518733312af16a44300404e945338981de3"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
-                "reference": "77a32518733312af16a44300404e945338981de3",
-                "shasum": ""
-            },
-            "require": {
-                "php": "^7.2 || ^8.0",
-                "phpdocumentor/reflection-common": "^2.0"
-            },
-            "require-dev": {
-                "ext-tokenizer": "*",
-                "psalm/phar": "^4.8"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-1.x": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "phpDocumentor\\Reflection\\": "src"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Mike van Riel",
-                    "email": "me@mikevanriel.com"
-                }
-            ],
-            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
-            "support": {
-                "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
-                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
-            },
-            "time": "2022-03-15T21:29:03+00:00"
-        },
-        {
-            "name": "phpspec/prophecy",
-            "version": "v1.15.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
-                "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/instantiator": "^1.2",
-                "php": "^7.2 || ~8.0, <8.2",
-                "phpdocumentor/reflection-docblock": "^5.2",
-                "sebastian/comparator": "^3.0 || ^4.0",
-                "sebastian/recursion-context": "^3.0 || ^4.0"
-            },
-            "require-dev": {
-                "phpspec/phpspec": "^6.0 || ^7.0",
-                "phpunit/phpunit": "^8.0 || ^9.0"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Prophecy\\": "src/Prophecy"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Konstantin Kudryashov",
-                    "email": "ever.zet@gmail.com",
-                    "homepage": "http://everzet.com"
-                },
-                {
-                    "name": "Marcello Duarte",
-                    "email": "marcello.duarte@gmail.com"
-                }
-            ],
-            "description": "Highly opinionated mocking framework for PHP 5.3+",
-            "homepage": "https://github.com/phpspec/prophecy",
-            "keywords": [
-                "Double",
-                "Dummy",
-                "fake",
-                "mock",
-                "spy",
-                "stub"
-            ],
-            "support": {
-                "issues": "https://github.com/phpspec/prophecy/issues",
-                "source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
-            },
-            "time": "2021-12-08T12:19:24+00:00"
-        },
         {
         {
             "name": "phpunit/php-code-coverage",
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.15",
+            "version": "9.2.16",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
+                "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
-                "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073",
+                "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "ext-dom": "*",
                 "ext-dom": "*",
                 "ext-libxml": "*",
                 "ext-libxml": "*",
                 "ext-xmlwriter": "*",
                 "ext-xmlwriter": "*",
-                "nikic/php-parser": "^4.13.0",
+                "nikic/php-parser": "^4.14",
                 "php": ">=7.3",
                 "php": ">=7.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-file-iterator": "^3.0.3",
                 "phpunit/php-text-template": "^2.0.2",
                 "phpunit/php-text-template": "^2.0.2",
@@ -9084,7 +8947,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -9092,7 +8955,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-03-07T09:28:20+00:00"
+            "time": "2022-08-20T05:26:47+00:00"
         },
         },
         {
         {
             "name": "phpunit/php-file-iterator",
             "name": "phpunit/php-file-iterator",
@@ -9337,16 +9200,16 @@
         },
         },
         {
         {
             "name": "phpunit/phpunit",
             "name": "phpunit/phpunit",
-            "version": "9.5.21",
+            "version": "9.5.23",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1"
+                "reference": "888556852e7e9bbeeedb9656afe46118765ade34"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1",
-                "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34",
+                "reference": "888556852e7e9bbeeedb9656afe46118765ade34",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -9361,7 +9224,6 @@
                 "phar-io/manifest": "^2.0.3",
                 "phar-io/manifest": "^2.0.3",
                 "phar-io/version": "^3.0.2",
                 "phar-io/version": "^3.0.2",
                 "php": ">=7.3",
                 "php": ">=7.3",
-                "phpspec/prophecy": "^1.12.1",
                 "phpunit/php-code-coverage": "^9.2.13",
                 "phpunit/php-code-coverage": "^9.2.13",
                 "phpunit/php-file-iterator": "^3.0.5",
                 "phpunit/php-file-iterator": "^3.0.5",
                 "phpunit/php-invoker": "^3.1.1",
                 "phpunit/php-invoker": "^3.1.1",
@@ -9379,9 +9241,6 @@
                 "sebastian/type": "^3.0",
                 "sebastian/type": "^3.0",
                 "sebastian/version": "^3.0.2"
                 "sebastian/version": "^3.0.2"
             },
             },
-            "require-dev": {
-                "phpspec/prophecy-phpunit": "^2.0.1"
-            },
             "suggest": {
             "suggest": {
                 "ext-soap": "*",
                 "ext-soap": "*",
                 "ext-xdebug": "*"
                 "ext-xdebug": "*"
@@ -9423,7 +9282,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -9435,7 +9294,7 @@
                     "type": "github"
                     "type": "github"
                 }
                 }
             ],
             ],
-            "time": "2022-06-19T12:14:25+00:00"
+            "time": "2022-08-22T14:01:36+00:00"
         },
         },
         {
         {
             "name": "pimple/pimple",
             "name": "pimple/pimple",
@@ -10886,16 +10745,16 @@
         },
         },
         {
         {
             "name": "spatie/ray",
             "name": "spatie/ray",
-            "version": "1.35.0",
+            "version": "1.36.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/spatie/ray.git",
                 "url": "https://github.com/spatie/ray.git",
-                "reference": "7196737c17718264aef9e446b773ee490c1563dd"
+                "reference": "4a4def8cda4806218341b8204c98375aa8c34323"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/ray/zipball/7196737c17718264aef9e446b773ee490c1563dd",
-                "reference": "7196737c17718264aef9e446b773ee490c1563dd",
+                "url": "https://api.github.com/repos/spatie/ray/zipball/4a4def8cda4806218341b8204c98375aa8c34323",
+                "reference": "4a4def8cda4806218341b8204c98375aa8c34323",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -10945,7 +10804,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/spatie/ray/issues",
                 "issues": "https://github.com/spatie/ray/issues",
-                "source": "https://github.com/spatie/ray/tree/1.35.0"
+                "source": "https://github.com/spatie/ray/tree/1.36.0"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -10957,7 +10816,7 @@
                     "type": "other"
                     "type": "other"
                 }
                 }
             ],
             ],
-            "time": "2022-08-09T14:35:12+00:00"
+            "time": "2022-08-11T14:04:18+00:00"
         },
         },
         {
         {
             "name": "symfony/filesystem",
             "name": "symfony/filesystem",

+ 1 - 1
config/google2fa.php

@@ -46,7 +46,7 @@ return [
     /*
     /*
      * Forbid user to reuse One Time Passwords.
      * Forbid user to reuse One Time Passwords.
      */
      */
-    'forbid_old_passwords' => false,
+    'forbid_old_passwords' => true,
 
 
     /*
     /*
      * User's table column for google2fa secret
      * User's table column for google2fa secret

文件差异内容过多而无法显示
+ 404 - 211
package-lock.json


+ 11 - 11
package.json

@@ -13,31 +13,31 @@
         "pre-commit": "lint-staged"
         "pre-commit": "lint-staged"
     },
     },
     "dependencies": {
     "dependencies": {
+        "@headlessui/vue": "^1.6.7",
+        "@kyvg/vue3-notification": "^2.3.4",
+        "@vueform/multiselect": "^2.3.2",
         "autoprefixer": "^10.4.1",
         "autoprefixer": "^10.4.1",
-        "axios": "^0.26.0",
+        "axios": "^0.27.0",
         "cross-env": "^7.0.3",
         "cross-env": "^7.0.3",
         "dayjs": "^1.10.4",
         "dayjs": "^1.10.4",
         "laravel-mix": "^6.0.11",
         "laravel-mix": "^6.0.11",
         "lodash": "^4.17.20",
         "lodash": "^4.17.20",
-        "portal-vue": "^2.1.7",
         "postcss": "^8.4.5",
         "postcss": "^8.4.5",
         "postcss-import": "^14.0.0",
         "postcss-import": "^14.0.0",
         "resolve-url-loader": "^5.0.0",
         "resolve-url-loader": "^5.0.0",
         "tailwindcss": "^3.0.11",
         "tailwindcss": "^3.0.11",
         "tippy.js": "^6.2.7",
         "tippy.js": "^6.2.7",
-        "v-clipboard": "^2.2.3",
-        "vue": "^2.6.12",
-        "vue-good-table": "^2.21.3",
-        "vue-loader": "^15.9.6",
-        "vue-multiselect": "^2.1.6",
-        "vue-notification": "^1.3.20",
+        "v-clipboard": "github:euvl/v-clipboard",
+        "vue": "^3.0.0",
+        "vue-good-table-next": "^0.2.1",
+        "vue-loader": "^17.0.0",
         "vue-template-compiler": "^2.6.12",
         "vue-template-compiler": "^2.6.12",
-        "vuedraggable": "^2.24.2"
+        "vuedraggable": "^4.1.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
         "css-loader": "^6.0.0",
         "css-loader": "^6.0.0",
-        "husky": "^7.0.0",
-        "lint-staged": "^12.0.0",
+        "husky": "^8.0.0",
+        "lint-staged": "^13.0.0",
         "prettier": "^2.2.1"
         "prettier": "^2.2.1"
     },
     },
     "lint-staged": {
     "lint-staged": {

+ 1 - 1
phpunit.xml

@@ -26,7 +26,7 @@
         <env name="APP_ENV" value="testing"/>
         <env name="APP_ENV" value="testing"/>
         <env name="BCRYPT_ROUNDS" value="4"/>
         <env name="BCRYPT_ROUNDS" value="4"/>
         <env name="CACHE_DRIVER" value="array"/>
         <env name="CACHE_DRIVER" value="array"/>
-        <env name="MAIL_DRIVER" value="array"/>
+        <env name="MAIL_MAILER" value="array"/>
         <env name="QUEUE_CONNECTION" value="sync"/>
         <env name="QUEUE_CONNECTION" value="sync"/>
         <env name="SESSION_DRIVER" value="array"/>
         <env name="SESSION_DRIVER" value="array"/>
         <env name="DB_CONNECTION" value="sqlite"/>
         <env name="DB_CONNECTION" value="sqlite"/>

+ 15 - 8
resources/css/app.css

@@ -31,20 +31,27 @@ html {
   @apply bg-yellow-100 border-yellow-600 text-yellow-800;
   @apply bg-yellow-100 border-yellow-600 text-yellow-800;
 }
 }
 
 
-.multiselect .multiselect__tag {
-  @apply bg-indigo-100 text-indigo-900;
+.multiselect .multiselect-dropdown::-webkit-scrollbar {
+  @apply w-2 h-2;
+}
+
+.multiselect .multiselect-dropdown::-webkit-scrollbar-thumb {
+  @apply bg-indigo-500 rounded-full;
 }
 }
 
 
-.multiselect .multiselect__tag-icon:focus,
-.multiselect .multiselect__tag-icon:hover {
-  @apply bg-indigo-400;
+.multiselect .multiselect-dropdown::-webkit-scrollbar-track {
+  @apply bg-indigo-50;
 }
 }
 
 
-.multiselect .multiselect__option--highlight {
-  @apply bg-indigo-500;
+.multiselect .multiselect-tag {
+  @apply bg-indigo-100 text-indigo-900;
+}
+
+.multiselect .multiselect-option.is-selected {
+  @apply bg-indigo-100 text-indigo-900;
 }
 }
 
 
-.multiselect .multiselect__option--selected.multiselect__option--highlight {
+.multiselect .multiselect-option.is-selected:hover {
   @apply bg-red-500;
   @apply bg-red-500;
 }
 }
 
 

+ 44 - 45
resources/js/app.js

@@ -9,59 +9,58 @@ dayjs.extend(advancedFormat)
 dayjs.extend(relativeTime)
 dayjs.extend(relativeTime)
 dayjs.extend(utc)
 dayjs.extend(utc)
 
 
-import Vue from 'vue'
+import { createApp } from 'vue'
 
 
-import PortalVue from 'portal-vue'
-import Clipboard from 'v-clipboard'
-import Notifications from 'vue-notification'
-import VueGoodTablePlugin from 'vue-good-table'
+import Clipboard from 'v-clipboard/src'
+import Notifications from '@kyvg/vue3-notification'
+import VueGoodTablePlugin from 'vue-good-table-next'
 
 
-Vue.use(PortalVue)
-Vue.use(Clipboard)
-Vue.use(Notifications)
-Vue.use(VueGoodTablePlugin)
+const app = createApp({
+  data() {
+    return {
+      mobileNavActive: false,
+    }
+  },
+})
 
 
-Vue.component('loader', require('./components/Loader.vue').default)
-Vue.component('dropdown', require('./components/DropdownNav.vue').default)
-Vue.component('icon', require('./components/Icon.vue').default)
+app.use(Clipboard)
+app.use(Notifications)
+app.use(VueGoodTablePlugin)
 
 
-Vue.component('aliases', require('./pages/Aliases.vue').default)
-Vue.component('recipients', require('./pages/Recipients.vue').default)
-Vue.component('domains', require('./pages/Domains.vue').default)
-Vue.component('usernames', require('./pages/Usernames.vue').default)
-Vue.component('rules', require('./pages/Rules.vue').default)
-Vue.component('failed-deliveries', require('./pages/FailedDeliveries.vue').default)
+app.component('loader', require('./components/Loader.vue').default)
+app.component('dropdown', require('./components/DropdownNav.vue').default)
+app.component('icon', require('./components/Icon.vue').default)
 
 
-Vue.component(
+app.component('aliases', require('./pages/Aliases.vue').default)
+app.component('recipients', require('./pages/Recipients.vue').default)
+app.component('domains', require('./pages/Domains.vue').default)
+app.component('usernames', require('./pages/Usernames.vue').default)
+app.component('rules', require('./pages/Rules.vue').default)
+app.component('failed-deliveries', require('./pages/FailedDeliveries.vue').default)
+
+app.component(
   'personal-access-tokens',
   'personal-access-tokens',
   require('./components/sanctum/PersonalAccessTokens.vue').default
   require('./components/sanctum/PersonalAccessTokens.vue').default
 )
 )
-Vue.component('webauthn-keys', require('./components/WebauthnKeys.vue').default)
-
-Vue.filter('formatDate', value => {
-  return dayjs.utc(value).local().format('Do MMM YYYY')
-})
+app.component('webauthn-keys', require('./components/WebauthnKeys.vue').default)
 
 
-Vue.filter('formatDateTime', value => {
-  return dayjs.utc(value).local().format('Do MMM YYYY h:mm A')
-})
-
-Vue.filter('timeAgo', value => {
-  return dayjs.utc(value).fromNow()
-})
-
-Vue.filter('truncate', (string, value) => {
-  if (value >= string.length) {
-    return string
-  }
-  return string.substring(0, value) + '...'
-})
-
-const app = new Vue({
-  el: '#app',
-  data() {
-    return {
-      mobileNavActive: false,
+// Global filters
+app.config.globalProperties.$filters = {
+  formatDate(value) {
+    return dayjs.utc(value).local().format('Do MMM YYYY')
+  },
+  formatDateTime(value) {
+    return dayjs.utc(value).local().format('Do MMM YYYY h:mm A')
+  },
+  timeAgo(value) {
+    return dayjs.utc(value).fromNow()
+  },
+  truncate(value, length) {
+    if (length >= value.length) {
+      return value
     }
     }
+    return value.substring(0, length) + '...'
   },
   },
-})
+}
+
+app.mount('#app')

+ 55 - 137
resources/js/components/Modal.vue

@@ -1,148 +1,66 @@
+<!-- This example requires Tailwind CSS v2.0+ -->
 <template>
 <template>
-  <portal to="modals">
-    <div
-      v-if="showModal"
-      class="fixed inset-0 flex justify-center"
-      :class="overflow ? 'overflow-auto' : 'items-center'"
-    >
-      <transition
-        @before-leave="backdropLeaving = true"
-        @after-leave="backdropLeaving = false"
-        enter-active-class="transition-all transition-fast ease-out-quad"
-        leave-active-class="transition-all transition-medium ease-in-quad"
-        enter-class="opacity-0"
-        enter-to-class="opacity-100"
-        leave-class="opacity-100"
-        leave-to-class="opacity-0"
-        appear
+  <TransitionRoot as="template" :show="open">
+    <Dialog as="div" class="relative z-10" @close="open = false">
+      <TransitionChild
+        as="template"
+        enter="ease-out duration-300"
+        enter-from="opacity-0"
+        enter-to="opacity-100"
+        leave="ease-in duration-200"
+        leave-from="opacity-100"
+        leave-to="opacity-0"
       >
       >
-        <div v-if="showBackdrop">
-          <div
-            class="inset-0 bg-black opacity-25"
-            :class="overflow ? 'fixed pointer-events-none' : 'absolute'"
-            @click="close"
-          ></div>
-        </div>
-      </transition>
+        <div class="fixed inset-0 bg-grey-500 bg-opacity-75 transition-opacity" />
+      </TransitionChild>
 
 
-      <transition
-        @before-leave="cardLeaving = true"
-        @after-leave="cardLeaving = false"
-        enter-active-class="transition-all transition-fast ease-out-quad"
-        leave-active-class="transition-all transition-medium ease-in-quad"
-        enter-class="opacity-0 scale-70"
-        enter-to-class="opacity-100 scale-100"
-        leave-class="opacity-100 scale-100"
-        leave-to-class="opacity-0 scale-70"
-        appear
-      >
-        <div v-if="showContent" class="relative">
-          <slot></slot>
+      <div class="fixed z-10 inset-0 overflow-y-auto">
+        <div
+          class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0"
+        >
+          <TransitionChild
+            as="template"
+            enter="ease-out duration-300"
+            enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+            enter-to="opacity-100 translate-y-0 sm:scale-100"
+            leave="ease-in duration-200"
+            leave-from="opacity-100 translate-y-0 sm:scale-100"
+            leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+          >
+            <DialogPanel
+              class="relative bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:w-full sm:p-6"
+              :class="maxWidth ? maxWidth : 'sm:max-w-lg'"
+            >
+              <div class="mt-3 text-center sm:mt-0 sm:text-left">
+                <DialogTitle
+                  as="h2"
+                  class="text-2xl leading-tight font-medium text-grey-900 border-b-2 border-grey-100 pb-4"
+                >
+                  <slot name="title"></slot>
+                </DialogTitle>
+                <div class="mt-2">
+                  <slot name="content"></slot>
+                </div>
+              </div>
+            </DialogPanel>
+          </TransitionChild>
         </div>
         </div>
-      </transition>
-    </div>
-  </portal>
+      </div>
+    </Dialog>
+  </TransitionRoot>
 </template>
 </template>
 
 
 <script>
 <script>
+import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
+
 export default {
 export default {
-  props: {
-    open: {
-      type: Boolean,
-      required: true,
-    },
-    overflow: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
-  },
-  data() {
-    return {
-      showModal: false,
-      showBackdrop: false,
-      showContent: false,
-      backdropLeaving: false,
-      cardLeaving: false,
-    }
-  },
-  created() {
-    const onEscape = e => {
-      if (this.open && e.keyCode === 27) {
-        this.close()
-      }
-    }
-    document.addEventListener('keydown', onEscape)
-    this.$once('hook:destroyed', () => {
-      document.removeEventListener('keydown', onEscape)
-    })
-  },
-  watch: {
-    open: {
-      handler: function (newValue) {
-        if (newValue) {
-          this.show()
-        } else {
-          this.close()
-        }
-      },
-      immediate: true,
-    },
-    leaving(newValue) {
-      if (newValue === false) {
-        this.showModal = false
-        this.$emit('close')
-      }
-    },
-  },
-  computed: {
-    leaving() {
-      return this.backdropLeaving || this.cardLeaving
-    },
-  },
-  methods: {
-    show() {
-      this.showModal = true
-      this.showBackdrop = true
-      this.showContent = true
-    },
-    close() {
-      this.showBackdrop = false
-      this.showContent = false
-    },
+  props: ['open', 'maxWidth'],
+  components: {
+    Dialog,
+    DialogPanel,
+    DialogTitle,
+    TransitionChild,
+    TransitionRoot,
   },
   },
 }
 }
 </script>
 </script>
-
-<style>
-.origin-top-right {
-  transform-origin: top right;
-}
-.transition-all {
-  transition-property: all;
-}
-.transition-fastest {
-  transition-duration: 50ms;
-}
-.transition-faster {
-  transition-duration: 100ms;
-}
-.transition-fast {
-  transition-duration: 150ms;
-}
-.transition-medium {
-  transition-duration: 200ms;
-}
-.ease-out-quad {
-  transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
-}
-.ease-in-quad {
-  transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
-}
-.scale-70 {
-  transform: scale(0.7);
-}
-.scale-100 {
-  transform: scale(1);
-}
-</style>

+ 24 - 54
resources/js/components/MoreOptions.vue

@@ -1,65 +1,35 @@
 <template>
 <template>
-  <div class="relative flex justify-end items-center" @keydown.escape="isOpen = false">
-    <button
-      ref="openOptions"
-      @click="isOpen = !isOpen"
-      :aria-expanded="isOpen"
-      id="project-options-menu-0"
-      aria-has-popup="true"
-      type="button"
-      class="w-8 h-8 bg-white inline-flex items-center justify-center text-grey-400 rounded-full hover:text-grey-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
-    >
-      <span class="sr-only">Open options</span>
-
-      <icon
-        name="more"
-        class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
-        aria-hidden="true"
-      />
-    </button>
+  <Menu as="div" class="relative inline-block text-left">
+    <div>
+      <MenuButton class="flex items-center text-grey-400 hover:text-grey-600 focus:outline-none">
+        <span class="sr-only">Open options</span>
+        <icon
+          name="more"
+          class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
+          aria-hidden="true"
+        />
+      </MenuButton>
+    </div>
 
 
     <transition
     <transition
       enter-active-class="transition ease-out duration-100"
       enter-active-class="transition ease-out duration-100"
-      enter-class="opacity-0 scale-95"
-      enter-to-class="opacity-100 scale-100"
+      enter-from-class="transform opacity-0 scale-95"
+      enter-to-class="transform opacity-100 scale-100"
       leave-active-class="transition ease-in duration-75"
       leave-active-class="transition ease-in duration-75"
-      leave-class="opacity-100 scale-100"
-      leave-to-class="opacity-0 scale-95"
+      leave-from-class="transform opacity-100 scale-100"
+      leave-to-class="transform opacity-0 scale-95"
     >
     >
-      <div
-        v-show="isOpen"
-        class="mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg z-10 bg-white ring-1 ring-black ring-opacity-5 divide-y divide-grey-200"
-        role="menu"
-        aria-orientation="vertical"
-        aria-labelledby="project-options-menu-0"
+      <MenuItems
+        class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10"
       >
       >
-        <slot></slot>
-      </div>
+        <div class="py-1">
+          <slot></slot>
+        </div>
+      </MenuItems>
     </transition>
     </transition>
-  </div>
+  </Menu>
 </template>
 </template>
 
 
-<script>
-export default {
-  data() {
-    return {
-      isOpen: false,
-    }
-  },
-  created() {
-    window.addEventListener('click', this.close)
-  },
-
-  beforeDestroy() {
-    window.removeEventListener('click', this.close)
-  },
-
-  methods: {
-    close(e) {
-      if (!this.$refs.openOptions.contains(e.target)) {
-        this.isOpen = false
-      }
-    },
-  },
-}
+<script setup>
+import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
 </script>
 </script>

+ 27 - 15
resources/js/components/Toggle.vue

@@ -1,19 +1,24 @@
 <template>
 <template>
-  <button
+  <Switch
+    v-model="modelValue"
     @click="toggle"
     @click="toggle"
-    type="button"
-    :aria-pressed="value.toString()"
-    :class="this.value ? 'bg-cyan-500' : 'bg-grey-300'"
-    class="relative inline-flex shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none"
+    :class="[
+      modelValue ? 'bg-cyan-500' : 'bg-grey-300',
+      'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none',
+    ]"
   >
   >
     <span class="sr-only">Use setting</span>
     <span class="sr-only">Use setting</span>
     <span
     <span
-      :class="this.value ? 'translate-x-5' : 'translate-x-0'"
-      class="relative inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition ease-in-out duration-200"
+      :class="[
+        modelValue ? 'translate-x-5' : 'translate-x-0',
+        'pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200',
+      ]"
     >
     >
       <span
       <span
-        :class="this.value ? 'opacity-0 ease-out duration-100' : 'opacity-100 ease-in duration-200'"
-        class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
+        :class="[
+          modelValue ? 'opacity-0 ease-out duration-100' : 'opacity-100 ease-in duration-200',
+          'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
+        ]"
         aria-hidden="true"
         aria-hidden="true"
       >
       >
         <svg class="h-3 w-3 text-grey-400" fill="none" viewBox="0 0 12 12">
         <svg class="h-3 w-3 text-grey-400" fill="none" viewBox="0 0 12 12">
@@ -27,8 +32,10 @@
         </svg>
         </svg>
       </span>
       </span>
       <span
       <span
-        :class="this.value ? 'opacity-100 ease-in duration-200' : 'opacity-0 ease-out duration-100'"
-        class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity"
+        :class="[
+          modelValue ? 'opacity-100 ease-in duration-200' : 'opacity-0 ease-out duration-100',
+          'absolute inset-0 h-full w-full flex items-center justify-center transition-opacity',
+        ]"
         aria-hidden="true"
         aria-hidden="true"
       >
       >
         <svg class="h-3 w-3 text-cyan-500" fill="currentColor" viewBox="0 0 12 12">
         <svg class="h-3 w-3 text-cyan-500" fill="currentColor" viewBox="0 0 12 12">
@@ -38,16 +45,21 @@
         </svg>
         </svg>
       </span>
       </span>
     </span>
     </span>
-  </button>
+  </Switch>
 </template>
 </template>
 
 
 <script>
 <script>
+import { Switch } from '@headlessui/vue'
+
 export default {
 export default {
-  props: ['value'],
+  props: ['modelValue'],
+  components: {
+    Switch,
+  },
   methods: {
   methods: {
     toggle() {
     toggle() {
-      this.$emit('input', !this.value)
-      this.value ? this.$emit('off') : this.$emit('on')
+      this.$emit('update:modelValue', !this.modelValue)
+      this.modelValue ? this.$emit('off') : this.$emit('on')
     },
     },
   },
   },
 }
 }

+ 4 - 8
resources/js/components/WebauthnKeys.vue

@@ -25,7 +25,7 @@
           </div>
           </div>
           <div v-for="key in keys" :key="key.id" class="table-row even:bg-grey-50 odd:bg-white">
           <div v-for="key in keys" :key="key.id" class="table-row even:bg-grey-50 odd:bg-white">
             <div class="table-cell p-1 md:p-4">{{ key.name }}</div>
             <div class="table-cell p-1 md:p-4">{{ key.name }}</div>
-            <div class="table-cell p-1 md:p-4">{{ key.created_at | timeAgo }}</div>
+            <div class="table-cell p-1 md:p-4">{{ $filters.timeAgo(key.created_at) }}</div>
             <div class="table-cell p-1 md:p-4">
             <div class="table-cell p-1 md:p-4">
               <Toggle v-model="key.enabled" @on="enableKey(key.id)" @off="disableKey(key.id)" />
               <Toggle v-model="key.enabled" @on="enableKey(key.id)" @off="disableKey(key.id)" />
             </div>
             </div>
@@ -43,12 +43,8 @@
     </div>
     </div>
 
 
     <Modal :open="deleteKeyModalOpen" @close="closeDeleteKeyModal">
     <Modal :open="deleteKeyModalOpen" @close="closeDeleteKeyModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Remove Hardware Key
-        </h2>
+      <template v-slot:title> Remove Hardware Key </template>
+      <template v-slot:content>
         <p v-if="keys.length === 1" class="my-4 text-grey-700">
         <p v-if="keys.length === 1" class="my-4 text-grey-700">
           Once this key is removed, <b>Two-Factor Authentication</b> will be disabled on your
           Once this key is removed, <b>Two-Factor Authentication</b> will be disabled on your
           account.
           account.
@@ -74,7 +70,7 @@
             Close
             Close
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>

+ 115 - 114
resources/js/components/sanctum/PersonalAccessTokens.vue

@@ -77,13 +77,13 @@
             class="table-row even:bg-grey-50 odd:bg-white"
             class="table-row even:bg-grey-50 odd:bg-white"
           >
           >
             <div class="table-cell p-1 md:p-4">{{ token.name }}</div>
             <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">{{ $filters.timeAgo(token.created_at) }}</div>
             <div v-if="token.last_used_at" class="table-cell p-1 md:p-4">
             <div v-if="token.last_used_at" class="table-cell p-1 md:p-4">
-              {{ token.last_used_at | timeAgo }}
+              {{ $filters.timeAgo(token.last_used_at) }}
             </div>
             </div>
             <div v-else class="table-cell p-1 md:p-4">Not used yet</div>
             <div v-else class="table-cell p-1 md:p-4">Not used yet</div>
             <div v-if="token.expires_at" class="table-cell p-1 md:p-4">
             <div v-if="token.expires_at" class="table-cell p-1 md:p-4">
-              {{ token.expires_at | formatDate }}
+              {{ $filters.formatDate(token.expires_at) }}
             </div>
             </div>
             <div v-else class="table-cell p-1 md:p-4">Does not expire</div>
             <div v-else class="table-cell p-1 md:p-4">Does not expire</div>
             <div class="table-cell p-1 md:p-4 text-right">
             <div class="table-cell p-1 md:p-4 text-right">
@@ -100,125 +100,122 @@
     </div>
     </div>
 
 
     <Modal :open="createTokenModalOpen" @close="closeCreateTokenModal">
     <Modal :open="createTokenModalOpen" @close="closeCreateTokenModal">
-      <div v-if="!accessToken" class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Create New Token
-        </h2>
-        <p class="mt-4 text-grey-700">
-          What's this token going to be used for? Give it a short name so that you remember later.
-          You can also select an expiry date for the token if you wish.
-        </p>
-        <div class="mt-6">
-          <div v-if="isObject(form.errors)" class="mb-3 text-red-500">
-            <ul>
-              <li v-for="error in form.errors" :key="error[0]">
-                {{ error[0] }}
-              </li>
-            </ul>
-          </div>
-          <label for="create-token-name" class="block text-grey-700 text-sm my-2"> Name: </label>
-          <input
-            v-model="form.name"
-            type="text"
-            id="create-token-name"
-            class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-4"
-            :class="form.errors.name ? 'border-red-500' : ''"
-            placeholder="e.g. Firefox extension"
-            autofocus
-          />
-          <label for="create-token-name" class="block text-grey-700 text-sm my-2">
-            Expiration:
-          </label>
-          <div class="block relative mb-6">
-            <select
-              v-model="form.expiration"
-              class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:ring"
-              :class="form.errors.expiration ? 'border border-red-500' : ''"
-            >
-              <option value="day">1 day</option>
-              <option value="week">1 week</option>
-              <option value="month">1 month</option>
-              <option value="year">1 year</option>
-              <option :value="null">No expiration</option>
-            </select>
-            <div
-              class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
-            >
-              <svg
-                class="fill-current h-4 w-4"
-                xmlns="http://www.w3.org/2000/svg"
-                viewBox="0 0 20 20"
+      <template v-if="!accessToken" v-slot:title> Create New Token </template>
+      <template v-else v-slot:title> Personal Access Token </template>
+      <template v-slot:content>
+        <div v-show="!accessToken">
+          <p class="mt-4 text-grey-700">
+            What's this token going to be used for? Give it a short name so that you remember later.
+            You can also select an expiry date for the token if you wish.
+          </p>
+          <div class="mt-6">
+            <div v-if="isObject(form.errors)" class="mb-3 text-red-500">
+              <ul>
+                <li v-for="error in form.errors" :key="error[0]">
+                  {{ error[0] }}
+                </li>
+              </ul>
+            </div>
+            <label for="create-token-name" class="block text-grey-700 text-sm my-2"> Name: </label>
+            <input
+              v-model="form.name"
+              type="text"
+              id="create-token-name"
+              class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-4"
+              :class="form.errors.name ? 'border-red-500' : ''"
+              placeholder="e.g. Firefox extension"
+              autofocus
+            />
+            <label for="create-token-name" class="block text-grey-700 text-sm my-2">
+              Expiration:
+            </label>
+            <div class="block relative mb-6">
+              <select
+                v-model="form.expiration"
+                class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:ring"
+                :class="form.errors.expiration ? 'border border-red-500' : ''"
+              >
+                <option value="day">1 day</option>
+                <option value="week">1 week</option>
+                <option value="month">1 month</option>
+                <option value="year">1 year</option>
+                <option :value="null">No expiration</option>
+              </select>
+              <div
+                class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
               >
               >
-                <path
-                  d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
-                />
-              </svg>
+                <svg
+                  class="fill-current h-4 w-4"
+                  xmlns="http://www.w3.org/2000/svg"
+                  viewBox="0 0 20 20"
+                >
+                  <path
+                    d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
+                  />
+                </svg>
+              </div>
             </div>
             </div>
+            <button
+              @click="store"
+              class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
+              :class="loading ? 'cursor-not-allowed' : ''"
+              :disabled="loading"
+            >
+              Create Token
+              <loader v-if="loading" />
+            </button>
+            <button
+              @click="closeCreateTokenModal"
+              class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
+            >
+              Close
+            </button>
           </div>
           </div>
-          <button
-            @click="store"
-            class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
-            :class="loading ? 'cursor-not-allowed' : ''"
-            :disabled="loading"
-          >
-            Create Token
-            <loader v-if="loading" />
-          </button>
-          <button
-            @click="closeCreateTokenModal"
-            class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
-          >
-            Close
-          </button>
         </div>
         </div>
-      </div>
-      <div v-else class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Personal Access Token
-        </h2>
-        <p class="my-4 text-grey-700">
-          This is your new personal access token. This is the only time the token will ever be
-          displayed, so please make a note of it in a safe place (e.g. password manager)!
-        </p>
-        <textarea
-          v-model="accessToken"
-          @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>
-        <div class="mt-6">
-          <button
-            class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
-            v-clipboard="() => accessToken"
-            v-clipboard:success="clipboardSuccess"
-            v-clipboard:error="clipboardError"
-          >
-            Copy To Clipboard
-          </button>
-          <button
-            @click="closeCreateTokenModal"
-            class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
+        <div v-show="accessToken">
+          <p class="my-4 text-grey-700">
+            This is your new personal access token. This is the only time the token will ever be
+            displayed, so please make a note of it in a safe place (e.g. password manager)!
+          </p>
+          <textarea
+            v-model="accessToken"
+            @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
           >
           >
-            Close
-          </button>
+          </textarea>
+          <div class="text-center">
+            <img :src="qrCode" class="inline-block" alt="QR Code" />
+            <p class="text-left text-sm text-grey-700">
+              You can scan this QR code to automatically login to the AnonAddy for Android mobile
+              app.
+            </p>
+          </div>
+          <div class="mt-6">
+            <button
+              class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
+              v-clipboard="() => accessToken"
+              v-clipboard:success="clipboardSuccess"
+              v-clipboard:error="clipboardError"
+            >
+              Copy To Clipboard
+            </button>
+            <button
+              @click="closeCreateTokenModal"
+              class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
+            >
+              Close
+            </button>
+          </div>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="revokeTokenModalOpen" @close="closeRevokeTokenModal">
     <Modal :open="revokeTokenModalOpen" @close="closeRevokeTokenModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Revoke API Access Token
-        </h2>
+      <template v-slot:title> ARevoke API Access Token </template>
+      <template v-slot:content>
         <p class="my-4 text-grey-700">
         <p class="my-4 text-grey-700">
           Any browser extension, application or script using this API access token will no longer be
           Any browser extension, application or script using this API access token will no longer be
           able to access the API. This action cannot be undone.
           able to access the API. This action cannot be undone.
@@ -240,7 +237,7 @@
             Close
             Close
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -255,6 +252,7 @@ export default {
   data() {
   data() {
     return {
     return {
       accessToken: null,
       accessToken: null,
+      qrCode: null,
       createTokenModalOpen: false,
       createTokenModalOpen: false,
       revokeTokenModalOpen: false,
       revokeTokenModalOpen: false,
       tokens: [],
       tokens: [],
@@ -285,6 +283,7 @@ export default {
     store() {
     store() {
       this.loading = true
       this.loading = true
       this.accessToken = null
       this.accessToken = null
+      this.qrCode = null
       this.form.errors = {}
       this.form.errors = {}
 
 
       axios
       axios
@@ -297,6 +296,7 @@ export default {
 
 
           this.tokens.push(response.data.token)
           this.tokens.push(response.data.token)
           this.accessToken = response.data.accessToken
           this.accessToken = response.data.accessToken
+          this.qrCode = response.data.qrCode
         })
         })
         .catch(error => {
         .catch(error => {
           this.loading = false
           this.loading = false
@@ -330,6 +330,7 @@ export default {
     },
     },
     openCreateTokenModal() {
     openCreateTokenModal() {
       this.accessToken = null
       this.accessToken = null
+      this.qrCode = null
       this.createTokenModalOpen = true
       this.createTokenModalOpen = true
     },
     },
     closeCreateTokenModal() {
     closeCreateTokenModal() {

+ 64 - 82
resources/js/pages/Aliases.vue

@@ -98,7 +98,7 @@
         />
         />
         <icon
         <icon
           v-if="search"
           v-if="search"
-          @click.native="search = ''"
+          @click="search = ''"
           name="close-circle"
           name="close-circle"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
         />
         />
@@ -146,9 +146,9 @@
 
 
     <vue-good-table
     <vue-good-table
       v-if="initialAliases.length"
       v-if="initialAliases.length"
-      @on-search="debounceToolips"
-      @on-page-change="debounceToolips"
-      @on-per-page-change="debounceToolips"
+      v-on:search="debounceToolips"
+      v-on:page-change="debounceToolips"
+      v-on:per-page-change="debounceToolips"
       :columns="columns"
       :columns="columns"
       :rows="rows"
       :rows="rows"
       :search-options="{
       :search-options="{
@@ -169,10 +169,10 @@
       }"
       }"
       styleClass="vgt-table"
       styleClass="vgt-table"
     >
     >
-      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+      <template #emptystate class="flex items-center justify-center h-24 text-lg text-grey-700">
         No aliases found for that search!
         No aliases found for that search!
-      </div>
-      <template slot="table-row" slot-scope="props">
+      </template>
+      <template #table-row="props">
         <span v-if="props.column.field == 'created_at'" class="flex items-center">
         <span v-if="props.column.field == 'created_at'" class="flex items-center">
           <span
           <span
             :class="`bg-${getAliasStatus(props.row).colour}-100`"
             :class="`bg-${getAliasStatus(props.row).colour}-100`"
@@ -187,8 +187,8 @@
           </span>
           </span>
           <span
           <span
             class="tooltip outline-none text-sm whitespace-nowrap"
             class="tooltip outline-none text-sm whitespace-nowrap"
-            :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
-            >{{ props.row.created_at | timeAgo }}
+            :data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
+            >{{ $filters.timeAgo(props.row.created_at) }}
           </span>
           </span>
         </span>
         </span>
         <span v-else-if="props.column.field == 'email'" class="block">
         <span v-else-if="props.column.field == 'email'" class="block">
@@ -199,10 +199,10 @@
             v-clipboard:success="clipboardSuccess"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:error="clipboardError"
             v-clipboard:error="clipboardError"
             ><span class="font-semibold text-indigo-800">{{
             ><span class="font-semibold text-indigo-800">{{
-              getAliasLocalPart(props.row) | truncate(60)
+              $filters.truncate(getAliasLocalPart(props.row), 60)
             }}</span
             }}</span
             ><span v-if="getAliasLocalPart(props.row).length <= 60">{{
             ><span v-if="getAliasLocalPart(props.row).length <= 60">{{
-              ('@' + props.row.domain) | truncate(60 - getAliasLocalPart(props.row).length)
+              $filters.truncate('@' + props.row.domain, 60 - getAliasLocalPart(props.row).length)
             }}</span>
             }}</span>
           </span>
           </span>
           <div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
           <div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
@@ -220,22 +220,22 @@
             <icon
             <icon
               name="close"
               name="close"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
-              @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
+              @click="aliasIdToEdit = aliasDescriptionToEdit = ''"
             />
             />
             <icon
             <icon
               name="save"
               name="save"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
-              @click.native="editAlias(rows[props.row.originalIndex])"
+              @click="editAlias(rows[props.row.originalIndex])"
             />
             />
           </div>
           </div>
           <div v-else-if="props.row.description" class="flex items-center">
           <div v-else-if="props.row.description" class="flex items-center">
             <span class="inline-block text-grey-400 text-sm py-1 border border-transparent">
             <span class="inline-block text-grey-400 text-sm py-1 border border-transparent">
-              {{ props.row.description | truncate(60) }}
+              {{ $filters.truncate(props.row.description, 60) }}
             </span>
             </span>
             <icon
             <icon
               name="edit"
               name="edit"
               class="inline-block w-6 h-6 ml-2 text-grey-300 fill-current cursor-pointer"
               class="inline-block w-6 h-6 ml-2 text-grey-300 fill-current cursor-pointer"
-              @click.native="
+              @click="
                 ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
                 ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
               "
               "
             />
             />
@@ -279,7 +279,7 @@
           <icon
           <icon
             name="edit"
             name="edit"
             class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-            @click.native="openAliasRecipientsModal(props.row)"
+            @click="openAliasRecipientsModal(props.row)"
           />
           />
         </span>
         </span>
         <span
         <span
@@ -311,16 +311,18 @@
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
           <more-options>
           <more-options>
             <div role="none">
             <div role="none">
-              <span
-                @click="openSendFromModal(props.row)"
-                class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
-                role="menuitem"
-              >
-                <icon name="send" class="block mr-3 w-5 h-5 text-grey-300 outline-none" />
-                Send From
-              </span>
+              <MenuItem>
+                <span
+                  @click="openSendFromModal(props.row)"
+                  class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
+                  role="menuitem"
+                >
+                  <icon name="send" class="block mr-3 w-5 h-5 text-grey-300 outline-none" />
+                  Send From
+                </span>
+              </MenuItem>
             </div>
             </div>
-            <div v-if="props.row.deleted_at" role="none">
+            <MenuItem v-if="props.row.deleted_at">
               <span
               <span
                 @click="openRestoreModal(props.row.id)"
                 @click="openRestoreModal(props.row.id)"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
@@ -332,8 +334,8 @@
                 />
                 />
                 Restore
                 Restore
               </span>
               </span>
-            </div>
-            <div v-else role="none">
+            </MenuItem>
+            <MenuItem v-else>
               <span
               <span
                 @click="openDeleteModal(props.row)"
                 @click="openDeleteModal(props.row)"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
@@ -345,8 +347,8 @@
                 />
                 />
                 Delete
                 Delete
               </span>
               </span>
-            </div>
-            <div role="none">
+            </MenuItem>
+            <MenuItem>
               <span
               <span
                 @click="openForgetModal(props.row)"
                 @click="openForgetModal(props.row)"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
                 class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
@@ -358,7 +360,7 @@
                 />
                 />
                 Forget
                 Forget
               </span>
               </span>
-            </div>
+            </MenuItem>
           </more-options>
           </more-options>
         </span>
         </span>
       </template>
       </template>
@@ -411,12 +413,8 @@
     </div>
     </div>
 
 
     <Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
     <Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Create new alias
-        </h2>
+      <template v-slot:title> Create new alias </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Other aliases e.g. alias@{{ subdomain }} can also be created automatically when they
           Other aliases e.g. alias@{{ subdomain }} can also be created automatically when they
           receive their first email.
           receive their first email.
@@ -524,6 +522,8 @@
         <multiselect
         <multiselect
           id="alias_recipient_ids"
           id="alias_recipient_ids"
           v-model="generateAliasRecipientIds"
           v-model="generateAliasRecipientIds"
+          mode="tags"
+          value-prop="id"
           :options="recipientOptions"
           :options="recipientOptions"
           :multiple="true"
           :multiple="true"
           :close-on-select="true"
           :close-on-select="true"
@@ -533,8 +533,6 @@
           placeholder="Select recipient(s) (optional)..."
           placeholder="Select recipient(s) (optional)..."
           label="email"
           label="email"
           track-by="email"
           track-by="email"
-          :preselect-first="false"
-          :show-labels="false"
         >
         >
         </multiselect>
         </multiselect>
 
 
@@ -555,33 +553,29 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
     <Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Update Alias Recipients
-        </h2>
+      <template v-slot:title> Update Alias Recipients </template>
+      <template v-slot:content>
         <p class="my-4 text-grey-700">
         <p class="my-4 text-grey-700">
           Select the recipients for this alias. You can choose multiple recipients. Leave it empty
           Select the recipients for this alias. You can choose multiple recipients. Leave it empty
           if you would like to use the default recipient.
           if you would like to use the default recipient.
         </p>
         </p>
         <multiselect
         <multiselect
           v-model="aliasRecipientsToEdit"
           v-model="aliasRecipientsToEdit"
+          mode="tags"
+          value-prop="id"
           :options="recipientOptions"
           :options="recipientOptions"
           :multiple="true"
           :multiple="true"
           :close-on-select="true"
           :close-on-select="true"
           :clear-on-select="false"
           :clear-on-select="false"
           :searchable="true"
           :searchable="true"
           :max="10"
           :max="10"
-          placeholder="Select recipients"
+          placeholder="Select recipient(s)"
           label="email"
           label="email"
           track-by="email"
           track-by="email"
-          :preselect-first="false"
-          :show-labels="false"
         >
         >
         </multiselect>
         </multiselect>
         <div class="mt-6">
         <div class="mt-6">
@@ -602,16 +596,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="restoreAliasModalOpen" @close="closeRestoreModal">
     <Modal :open="restoreAliasModalOpen" @close="closeRestoreModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Restore alias
-        </h2>
+      <template v-slot:title> Restore alias </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to restore this alias? Once restored it will be
           Are you sure you want to restore this alias? Once restored it will be
           <b>able to receive emails again</b>.
           <b>able to receive emails again</b>.
@@ -634,16 +624,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete alias
-        </h2>
+      <template v-slot:title> Delete alias </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to delete <b class="break-words">{{ aliasToDelete.email }}</b
           Are you sure you want to delete <b class="break-words">{{ aliasToDelete.email }}</b
           >? You can restore it if you later change your mind. Once deleted,
           >? You can restore it if you later change your mind. Once deleted,
@@ -668,16 +654,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="forgetAliasModalOpen" @close="closeForgetModal">
     <Modal :open="forgetAliasModalOpen" @close="closeForgetModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Forget alias
-        </h2>
+      <template v-slot:title> Forget alias </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to forget <b class="break-words">{{ aliasToForget.email }}</b
           Are you sure you want to forget <b class="break-words">{{ aliasToForget.email }}</b
           >? Forgetting an alias will disassociate it from your account.
           >? Forgetting an alias will disassociate it from your account.
@@ -705,16 +687,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="sendFromAliasModalOpen" @close="closeSendFromModal">
     <Modal :open="sendFromAliasModalOpen" @close="closeSendFromModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Send from alias
-        </h2>
+      <template v-slot:title> Send from alias </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Use this to automatically create the correct address to send an email to in order to send
           Use this to automatically create the correct address to send an email to in order to send
           an <b>email from this alias</b>.
           an <b>email from this alias</b>.
@@ -811,7 +789,7 @@
             Close
             Close
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -824,7 +802,8 @@ import { roundArrow } from 'tippy.js'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/tippy.css'
 import 'tippy.js/dist/tippy.css'
 import tippy from 'tippy.js'
 import tippy from 'tippy.js'
-import Multiselect from 'vue-multiselect'
+import Multiselect from '@vueform/multiselect'
+import { MenuItem } from '@headlessui/vue'
 
 
 export default {
 export default {
   props: {
   props: {
@@ -886,6 +865,7 @@ export default {
     Toggle,
     Toggle,
     Multiselect,
     Multiselect,
     MoreOptions,
     MoreOptions,
+    MenuItem,
   },
   },
   data() {
   data() {
     return {
     return {
@@ -1134,7 +1114,7 @@ export default {
     openAliasRecipientsModal(alias) {
     openAliasRecipientsModal(alias) {
       this.editAliasRecipientsModalOpen = true
       this.editAliasRecipientsModalOpen = true
       this.recipientsAliasToEdit = alias
       this.recipientsAliasToEdit = alias
-      this.aliasRecipientsToEdit = alias.recipients
+      this.aliasRecipientsToEdit = _.map(alias.recipients, recipient => recipient.id)
     },
     },
     closeAliasRecipientsModal() {
     closeAliasRecipientsModal() {
       this.editAliasRecipientsModalOpen = false
       this.editAliasRecipientsModalOpen = false
@@ -1149,7 +1129,7 @@ export default {
           '/api/v1/alias-recipients',
           '/api/v1/alias-recipients',
           JSON.stringify({
           JSON.stringify({
             alias_id: this.recipientsAliasToEdit.id,
             alias_id: this.recipientsAliasToEdit.id,
-            recipient_ids: _.map(this.aliasRecipientsToEdit, recipient => recipient.id),
+            recipient_ids: this.aliasRecipientsToEdit,
           }),
           }),
           {
           {
             headers: { 'Content-Type': 'application/json' },
             headers: { 'Content-Type': 'application/json' },
@@ -1157,7 +1137,9 @@ export default {
         )
         )
         .then(response => {
         .then(response => {
           let alias = _.find(this.rows, ['id', this.recipientsAliasToEdit.id])
           let alias = _.find(this.rows, ['id', this.recipientsAliasToEdit.id])
-          alias.recipients = this.aliasRecipientsToEdit
+          alias.recipients = _.filter(this.recipientOptions, recipient =>
+            this.aliasRecipientsToEdit.includes(recipient.id)
+          )
 
 
           this.editAliasRecipientsModalOpen = false
           this.editAliasRecipientsModalOpen = false
           this.editAliasRecipientsLoading = false
           this.editAliasRecipientsLoading = false
@@ -1362,4 +1344,4 @@ export default {
 }
 }
 </script>
 </script>
 
 
-<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
+<style src="@vueform/multiselect/themes/default.css"></style>

+ 42 - 53
resources/js/pages/Domains.vue

@@ -12,7 +12,7 @@
         />
         />
         <icon
         <icon
           v-if="search"
           v-if="search"
-          @click.native="search = ''"
+          @click="search = ''"
           name="close-circle"
           name="close-circle"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
         />
         />
@@ -34,7 +34,7 @@
 
 
     <vue-good-table
     <vue-good-table
       v-if="initialDomains.length"
       v-if="initialDomains.length"
-      @on-search="debounceToolips"
+      v-on:search="debounceToolips"
       :columns="columns"
       :columns="columns"
       :rows="rows"
       :rows="rows"
       :search-options="{
       :search-options="{
@@ -48,15 +48,15 @@
       }"
       }"
       styleClass="vgt-table"
       styleClass="vgt-table"
     >
     >
-      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+      <template #emptystate class="flex items-center justify-center h-24 text-lg text-grey-700">
         No domains found for that search!
         No domains found for that search!
-      </div>
-      <template slot="table-row" slot-scope="props">
+      </template>
+      <template #table-row="props">
         <span
         <span
           v-if="props.column.field == 'created_at'"
           v-if="props.column.field == 'created_at'"
           class="tooltip outline-none text-sm"
           class="tooltip outline-none text-sm"
-          :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
-          >{{ props.row.created_at | timeAgo }}
+          :data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
+          >{{ $filters.timeAgo(props.row.created_at) }}
         </span>
         </span>
         <span v-else-if="props.column.field == 'domain'">
         <span v-else-if="props.column.field == 'domain'">
           <span
           <span
@@ -65,7 +65,7 @@
             v-clipboard="() => rows[props.row.originalIndex].domain"
             v-clipboard="() => rows[props.row.originalIndex].domain"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:error="clipboardError"
             v-clipboard:error="clipboardError"
-            >{{ props.row.domain | truncate(30) }}</span
+            >{{ $filters.truncate(props.row.domain, 30) }}</span
           >
           >
         </span>
         </span>
         <span v-else-if="props.column.field == 'description'">
         <span v-else-if="props.column.field == 'description'">
@@ -86,20 +86,20 @@
             <icon
             <icon
               name="close"
               name="close"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
-              @click.native="domainIdToEdit = domainDescriptionToEdit = ''"
+              @click="domainIdToEdit = domainDescriptionToEdit = ''"
             />
             />
             <icon
             <icon
               name="save"
               name="save"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
-              @click.native="editDomain(rows[props.row.originalIndex])"
+              @click="editDomain(rows[props.row.originalIndex])"
             />
             />
           </div>
           </div>
           <div v-else-if="props.row.description" class="flex items-centers">
           <div v-else-if="props.row.description" class="flex items-centers">
-            <span class="outline-none">{{ props.row.description | truncate(60) }}</span>
+            <span class="outline-none">{{ $filters.truncate(props.row.description, 60) }}</span>
             <icon
             <icon
               name="edit"
               name="edit"
               class="inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer ml-2"
               class="inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer ml-2"
-              @click.native="
+              @click="
                 ;(domainIdToEdit = props.row.id), (domainDescriptionToEdit = props.row.description)
                 ;(domainIdToEdit = props.row.id), (domainDescriptionToEdit = props.row.description)
               "
               "
             />
             />
@@ -108,24 +108,24 @@
             <icon
             <icon
               name="plus"
               name="plus"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native=";(domainIdToEdit = props.row.id), (domainDescriptionToEdit = '')"
+              @click=";(domainIdToEdit = props.row.id), (domainDescriptionToEdit = '')"
             />
             />
           </div>
           </div>
         </span>
         </span>
         <span v-else-if="props.column.field === 'default_recipient'">
         <span v-else-if="props.column.field === 'default_recipient'">
           <div v-if="props.row.default_recipient">
           <div v-if="props.row.default_recipient">
-            {{ props.row.default_recipient.email | truncate(30) }}
+            {{ $filters.truncate(props.row.default_recipient.email, 30) }}
             <icon
             <icon
               name="edit"
               name="edit"
               class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native="openDomainDefaultRecipientModal(props.row)"
+              @click="openDomainDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
           <div class="flex justify-center" v-else>
           <div class="flex justify-center" v-else>
             <icon
             <icon
               name="plus"
               name="plus"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native="openDomainDefaultRecipientModal(props.row)"
+              @click="openDomainDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
         </span>
         </span>
@@ -234,7 +234,7 @@
           <icon
           <icon
             name="trash"
             name="trash"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-            @click.native="openDeleteModal(props.row.id)"
+            @click="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>
       </template>
       </template>
@@ -265,12 +265,9 @@
     </div>
     </div>
 
 
     <Modal :open="addDomainModalOpen" @close="closeCheckRecordsModal">
     <Modal :open="addDomainModalOpen" @close="closeCheckRecordsModal">
-      <div v-if="!domainToCheck" class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Add new domain
-        </h2>
+      <template v-if="!domainToCheck" v-slot:title> Add new domain </template>
+      <template v-else v-slot:title> Check DNS records </template>
+      <template v-if="!domainToCheck" v-slot:content>
         <p class="mt-4 mb-2 text-grey-700">
         <p class="mt-4 mb-2 text-grey-700">
           To verify ownership of the domain, please add the following TXT record and then click Add
           To verify ownership of the domain, please add the following TXT record and then click Add
           Domain below.
           Domain below.
@@ -315,8 +312,8 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
-      <div v-else class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
+      </template>
+      <template v-else v-slot:content>
         <h2
         <h2
           class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
           class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
         >
         >
@@ -370,25 +367,22 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="domainDefaultRecipientModalOpen" @close="closeDomainDefaultRecipientModal">
     <Modal :open="domainDefaultRecipientModalOpen" @close="closeDomainDefaultRecipientModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Update Default Recipient
-        </h2>
+      <template v-slot:title> Update Default Recipient </template>
+      <template v-slot:content>
         <p class="my-4 text-grey-700">
         <p class="my-4 text-grey-700">
           Select the default recipient for this domain. This overrides the default recipient in your
           Select the default recipient for this domain. This overrides the default recipient in your
           account settings. Leave it empty if you would like to use the default recipient in your
           account settings. Leave it empty if you would like to use the default recipient in your
           account settings.
           account settings.
         </p>
         </p>
         <multiselect
         <multiselect
-          v-model="defaultRecipient"
+          v-model="defaultRecipientId"
           :options="recipientOptions"
           :options="recipientOptions"
-          :multiple="false"
+          mode="single"
+          value-prop="id"
           :close-on-select="true"
           :close-on-select="true"
           :clear-on-select="false"
           :clear-on-select="false"
           :searchable="false"
           :searchable="false"
@@ -396,8 +390,6 @@
           placeholder="Select recipient"
           placeholder="Select recipient"
           label="email"
           label="email"
           track-by="email"
           track-by="email"
-          :preselect-first="false"
-          :show-labels="false"
         >
         >
         </multiselect>
         </multiselect>
         <div class="mt-6">
         <div class="mt-6">
@@ -418,16 +410,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteDomainModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteDomainModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete domain
-        </h2>
+      <template v-slot:title> Delete domain </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to delete this domain? This will also delete all aliases associated
           Are you sure you want to delete this domain? This will also delete all aliases associated
           with this domain. You will no longer be able to receive any emails at this domain.
           with this domain. You will no longer be able to receive any emails at this domain.
@@ -450,7 +438,7 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -462,7 +450,7 @@ import { roundArrow } from 'tippy.js'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/tippy.css'
 import 'tippy.js/dist/tippy.css'
 import tippy from 'tippy.js'
 import tippy from 'tippy.js'
-import Multiselect from 'vue-multiselect'
+import Multiselect from '@vueform/multiselect'
 
 
 export default {
 export default {
   props: {
   props: {
@@ -507,7 +495,7 @@ export default {
       checkRecordsLoading: false,
       checkRecordsLoading: false,
       domainDefaultRecipientModalOpen: false,
       domainDefaultRecipientModalOpen: false,
       defaultRecipientDomainToEdit: {},
       defaultRecipientDomainToEdit: {},
-      defaultRecipient: {},
+      defaultRecipientId: null,
       editDefaultRecipientLoading: false,
       editDefaultRecipientLoading: false,
       errors: {},
       errors: {},
       columns: [
       columns: [
@@ -667,20 +655,20 @@ export default {
     openDomainDefaultRecipientModal(domain) {
     openDomainDefaultRecipientModal(domain) {
       this.domainDefaultRecipientModalOpen = true
       this.domainDefaultRecipientModalOpen = true
       this.defaultRecipientDomainToEdit = domain
       this.defaultRecipientDomainToEdit = domain
-      this.defaultRecipient = domain.default_recipient
+      this.defaultRecipientId = domain.default_recipient_id
     },
     },
     closeDomainDefaultRecipientModal() {
     closeDomainDefaultRecipientModal() {
       this.domainDefaultRecipientModalOpen = false
       this.domainDefaultRecipientModalOpen = false
       this.defaultRecipientDomainToEdit = {}
       this.defaultRecipientDomainToEdit = {}
-      this.defaultRecipient = {}
+      this.defaultRecipientId = null
     },
     },
     openCheckRecordsModal(domain) {
     openCheckRecordsModal(domain) {
       this.domainToCheck = domain
       this.domainToCheck = domain
       this.addDomainModalOpen = true
       this.addDomainModalOpen = true
     },
     },
     closeCheckRecordsModal() {
     closeCheckRecordsModal() {
-      this.domainToCheck = null
       this.addDomainModalOpen = false
       this.addDomainModalOpen = false
+      _.delay(() => (this.domainToCheck = null), 300)
     },
     },
     editDomain(domain) {
     editDomain(domain) {
       if (this.domainDescriptionToEdit.length > 200) {
       if (this.domainDescriptionToEdit.length > 200) {
@@ -716,7 +704,7 @@ export default {
         .patch(
         .patch(
           `/api/v1/domains/${this.defaultRecipientDomainToEdit.id}/default-recipient`,
           `/api/v1/domains/${this.defaultRecipientDomainToEdit.id}/default-recipient`,
           JSON.stringify({
           JSON.stringify({
-            default_recipient: this.defaultRecipient ? this.defaultRecipient.id : '',
+            default_recipient: this.defaultRecipientId,
           }),
           }),
           {
           {
             headers: { 'Content-Type': 'application/json' },
             headers: { 'Content-Type': 'application/json' },
@@ -724,17 +712,18 @@ export default {
         )
         )
         .then(response => {
         .then(response => {
           let domain = _.find(this.rows, ['id', this.defaultRecipientDomainToEdit.id])
           let domain = _.find(this.rows, ['id', this.defaultRecipientDomainToEdit.id])
-          domain.default_recipient = this.defaultRecipient
+          domain.default_recipient = _.find(this.recipientOptions, ['id', this.defaultRecipientId])
+          domain.default_recipient_id = this.defaultRecipientId
 
 
           this.domainDefaultRecipientModalOpen = false
           this.domainDefaultRecipientModalOpen = false
           this.editDefaultRecipientLoading = false
           this.editDefaultRecipientLoading = false
-          this.defaultRecipient = {}
+          this.defaultRecipientId = null
           this.success("Domain's default recipient updated")
           this.success("Domain's default recipient updated")
         })
         })
         .catch(error => {
         .catch(error => {
           this.domainDefaultRecipientModalOpen = false
           this.domainDefaultRecipientModalOpen = false
           this.editDefaultRecipientLoading = false
           this.editDefaultRecipientLoading = false
-          this.defaultRecipient = {}
+          this.defaultRecipientId = null
           this.error()
           this.error()
         })
         })
     },
     },

+ 13 - 17
resources/js/pages/FailedDeliveries.vue

@@ -12,7 +12,7 @@
         />
         />
         <icon
         <icon
           v-if="search"
           v-if="search"
-          @click.native="search = ''"
+          @click="search = ''"
           name="close-circle"
           name="close-circle"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
         />
         />
@@ -26,7 +26,7 @@
 
 
     <vue-good-table
     <vue-good-table
       v-if="initialFailedDeliveries.length"
       v-if="initialFailedDeliveries.length"
-      @on-search="debounceToolips"
+      v-on:search="debounceToolips"
       :columns="columns"
       :columns="columns"
       :rows="rows"
       :rows="rows"
       :search-options="{
       :search-options="{
@@ -40,15 +40,15 @@
       }"
       }"
       styleClass="vgt-table"
       styleClass="vgt-table"
     >
     >
-      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+      <template #emptystate class="flex items-center justify-center h-24 text-lg text-grey-700">
         No failed deliveries found for that search!
         No failed deliveries found for that search!
-      </div>
-      <template slot="table-row" slot-scope="props">
+      </template>
+      <template #table-row="props">
         <span
         <span
           v-if="props.column.field == 'created_at'"
           v-if="props.column.field == 'created_at'"
           class="tooltip outline-none text-sm"
           class="tooltip outline-none text-sm"
-          :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
-          >{{ props.row.created_at | timeAgo }}
+          :data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
+          >{{ $filters.timeAgo(props.row.created_at) }}
         </span>
         </span>
         <span v-else-if="props.column.field == 'recipient'">
         <span v-else-if="props.column.field == 'recipient'">
           <span
           <span
@@ -106,14 +106,14 @@
         <span
         <span
           v-else-if="props.column.field == 'attempted_at'"
           v-else-if="props.column.field == 'attempted_at'"
           class="tooltip outline-none text-sm"
           class="tooltip outline-none text-sm"
-          :data-tippy-content="rows[props.row.originalIndex].attempted_at | formatDateTime"
-          >{{ props.row.attempted_at | timeAgo }}
+          :data-tippy-content="$filters.formatDateTime(rows[props.row.originalIndex].attempted_at)"
+          >{{ $filters.timeAgo(props.row.attempted_at) }}
         </span>
         </span>
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
           <icon
           <icon
             name="trash"
             name="trash"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-            @click.native="openDeleteModal(props.row.id)"
+            @click="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>
       </template>
       </template>
@@ -137,12 +137,8 @@
     </div>
     </div>
 
 
     <Modal :open="deleteFailedDeliveryModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteFailedDeliveryModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete Failed Delivery
-        </h2>
+      <template v-slot:title> Delete Failed Delivery </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this failed delivery?</p>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this failed delivery?</p>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Failed deliveries are automatically removed when they are more than 3 days old.
           Failed deliveries are automatically removed when they are more than 3 days old.
@@ -165,7 +161,7 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>

+ 25 - 40
resources/js/pages/Recipients.vue

@@ -12,7 +12,7 @@
         />
         />
         <icon
         <icon
           v-if="search"
           v-if="search"
-          @click.native="search = ''"
+          @click="search = ''"
           name="close-circle"
           name="close-circle"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
         />
         />
@@ -33,7 +33,7 @@
     </div>
     </div>
 
 
     <vue-good-table
     <vue-good-table
-      @on-search="debounceToolips"
+      v-on:search="debounceToolips"
       :columns="columns"
       :columns="columns"
       :rows="rows"
       :rows="rows"
       :search-options="{
       :search-options="{
@@ -47,10 +47,10 @@
       }"
       }"
       styleClass="vgt-table"
       styleClass="vgt-table"
     >
     >
-      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+      <template #emptystate class="flex items-center justify-center h-24 text-lg text-grey-700">
         No recipients found for that search!
         No recipients found for that search!
-      </div>
-      <template slot="table-column" slot-scope="props">
+      </template>
+      <template #table-column="props">
         <span v-if="props.column.label == 'Key'">
         <span v-if="props.column.label == 'Key'">
           Key
           Key
           <span
           <span
@@ -82,12 +82,12 @@
           {{ props.column.label }}
           {{ props.column.label }}
         </span>
         </span>
       </template>
       </template>
-      <template slot="table-row" slot-scope="props">
+      <template #table-row="props">
         <span
         <span
           v-if="props.column.field == 'created_at'"
           v-if="props.column.field == 'created_at'"
           class="tooltip outline-none text-sm"
           class="tooltip outline-none text-sm"
-          :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
-          >{{ props.row.created_at | timeAgo }}
+          :data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
+          >{{ $filters.timeAgo(props.row.created_at) }}
         </span>
         </span>
         <span v-else-if="props.column.field == 'key'">
         <span v-else-if="props.column.field == 'key'">
           {{ props.row.key }}
           {{ props.row.key }}
@@ -99,7 +99,7 @@
             v-clipboard="() => rows[props.row.originalIndex].email"
             v-clipboard="() => rows[props.row.originalIndex].email"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:error="clipboardError"
             v-clipboard:error="clipboardError"
-            >{{ props.row.email | truncate(30) }}</span
+            >{{ $filters.truncate(props.row.email, 30) }}</span
           >
           >
 
 
           <span
           <span
@@ -115,7 +115,7 @@
             v-if="props.row.aliases.length"
             v-if="props.row.aliases.length"
             class="tooltip outline-none"
             class="tooltip outline-none"
             :data-tippy-content="aliasesTooltip(props.row.aliases, isDefault(props.row.id))"
             :data-tippy-content="aliasesTooltip(props.row.aliases, isDefault(props.row.id))"
-            >{{ props.row.aliases[0].email | truncate(40) }}
+            >{{ $filters.truncate(props.row.aliases[0].email, 40) }}
             <span
             <span
               v-if="isDefault(props.row.id) && aliasesUsingDefaultCount > 1"
               v-if="isDefault(props.row.id) && aliasesUsingDefaultCount > 1"
               class="block text-grey-500 text-sm"
               class="block text-grey-500 text-sm"
@@ -156,7 +156,7 @@
             <icon
             <icon
               name="delete"
               name="delete"
               class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current"
               class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current"
-              @click.native="openDeleteRecipientKeyModal(props.row)"
+              @click="openDeleteRecipientKeyModal(props.row)"
               data-tippy-content="Remove public key"
               data-tippy-content="Remove public key"
             />
             />
           </span>
           </span>
@@ -213,19 +213,15 @@
             v-if="!isDefault(props.row.id)"
             v-if="!isDefault(props.row.id)"
             name="trash"
             name="trash"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-            @click.native="openDeleteModal(props.row)"
+            @click="openDeleteModal(props.row)"
           />
           />
         </span>
         </span>
       </template>
       </template>
     </vue-good-table>
     </vue-good-table>
 
 
     <Modal :open="addRecipientModalOpen" @close="addRecipientModalOpen = false">
     <Modal :open="addRecipientModalOpen" @close="addRecipientModalOpen = false">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Add new recipient
-        </h2>
+      <template v-slot:title> Add new recipient </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Enter the individual email of the new recipient you'd like to add.
           Enter the individual email of the new recipient you'd like to add.
         </p>
         </p>
@@ -261,16 +257,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="addRecipientKeyModalOpen" @close="closeRecipientKeyModal">
     <Modal :open="addRecipientKeyModalOpen" @close="closeRecipientKeyModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Add Public GPG Key
-        </h2>
+      <template v-slot:title> Add Public GPG Key </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">Enter your <b>PUBLIC</b> key data in the text area below.</p>
         <p class="mt-4 text-grey-700">Enter your <b>PUBLIC</b> key data in the text area below.</p>
         <p class="mt-4 text-grey-700">Make sure to remove <b>Comment:</b> and <b>Version:</b></p>
         <p class="mt-4 text-grey-700">Make sure to remove <b>Comment:</b> and <b>Version:</b></p>
         <div class="mt-6">
         <div class="mt-6">
@@ -303,16 +295,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteRecipientKeyModalOpen" @close="closeDeleteRecipientKeyModal">
     <Modal :open="deleteRecipientKeyModalOpen" @close="closeDeleteRecipientKeyModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Remove recipient public key
-        </h2>
+      <template v-slot:title> Remove recipient public key </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to remove the public key for this recipient? It will also be removed
           Are you sure you want to remove the public key for this recipient? It will also be removed
           from any other recipients using the same key.
           from any other recipients using the same key.
@@ -335,16 +323,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete recipient
-        </h2>
+      <template v-slot:title> Delete recipient </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
         <div class="mt-6">
         <div class="mt-6">
           <button
           <button
@@ -364,7 +348,7 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -461,6 +445,7 @@ export default {
           field: 'should_encrypt',
           field: 'should_encrypt',
           type: 'boolean',
           type: 'boolean',
           globalSearchDisabled: true,
           globalSearchDisabled: true,
+          sortable: false,
         },
         },
         {
         {
           label: 'Inline Encryption',
           label: 'Inline Encryption',

+ 29 - 51
resources/js/pages/Rules.vue

@@ -15,32 +15,30 @@
 
 
     <div v-if="initialRules.length" class="bg-white shadow">
     <div v-if="initialRules.length" class="bg-white shadow">
       <draggable
       <draggable
-        tag="ul"
+        :component-data="{ name: 'flip-list' }"
+        item-key="id"
         v-model="rows"
         v-model="rows"
-        v-bind="dragOptions"
+        :group="{ name: 'description' }"
+        ghost-class="ghost"
         handle=".handle"
         handle=".handle"
         @change="reorderRules"
         @change="reorderRules"
       >
       >
-        <transition-group type="transition" name="flip-list">
-          <li
-            class="relative flex items-center py-3 px-5 border-b border-grey-100"
-            v-for="row in rows"
-            :key="row.name"
-          >
+        <template #item="{ element }">
+          <div class="relative flex items-center py-3 px-5 border-b border-grey-100">
             <div class="flex items-center w-3/5">
             <div class="flex items-center w-3/5">
               <icon
               <icon
                 name="menu"
                 name="menu"
                 class="handle block w-6 h-6 text-grey-300 fill-current cursor-pointer"
                 class="handle block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               />
               />
 
 
-              <span class="m-4">{{ row.name }} </span>
+              <span class="m-4">{{ element.name }} </span>
             </div>
             </div>
 
 
             <div class="w-1/5 relative flex">
             <div class="w-1/5 relative flex">
               <Toggle
               <Toggle
-                v-model="row.active"
-                @on="activateRule(row.id)"
-                @off="deactivateRule(row.id)"
+                v-model="element.active"
+                @on="activateRule(element.id)"
+                @off="deactivateRule(element.id)"
               />
               />
             </div>
             </div>
 
 
@@ -48,16 +46,16 @@
               <icon
               <icon
                 name="edit"
                 name="edit"
                 class="block w-6 h-6 mr-3 text-grey-300 fill-current cursor-pointer"
                 class="block w-6 h-6 mr-3 text-grey-300 fill-current cursor-pointer"
-                @click.native="openEditModal(row)"
+                @click="openEditModal(element)"
               />
               />
               <icon
               <icon
                 name="trash"
                 name="trash"
                 class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
                 class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-                @click.native="openDeleteModal(row.id)"
+                @click="openDeleteModal(element.id)"
               />
               />
             </div>
             </div>
-          </li>
-        </transition-group>
+          </div>
+        </template>
       </draggable>
       </draggable>
     </div>
     </div>
 
 
@@ -81,12 +79,8 @@
     </div>
     </div>
 
 
     <Modal :open="createRuleModalOpen" @close="createRuleModalOpen = false" :overflow="true">
     <Modal :open="createRuleModalOpen" @close="createRuleModalOpen = false" :overflow="true">
-      <div class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6 my-12">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Create new rule
-        </h2>
+      <template v-slot:title> Create new rule </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Rules work on all emails, including replies and also send froms. New conditions and
           Rules work on all emails, including replies and also send froms. New conditions and
           actions will be added over time.
           actions will be added over time.
@@ -234,7 +228,7 @@
                     v-if="createRuleObject.conditions.length > 1"
                     v-if="createRuleObject.conditions.length > 1"
                     name="trash"
                     name="trash"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
-                    @click.native="deleteCondition(createRuleObject, key)"
+                    @click="deleteCondition(createRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
               </div>
               </div>
@@ -248,7 +242,7 @@
                     <icon
                     <icon
                       name="close"
                       name="close"
                       class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
                       class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
-                      @click.native="createRuleObject.conditions[key].values.splice(index, 1)"
+                      @click="createRuleObject.conditions[key].values.splice(index, 1)"
                     />
                     />
                   </span>
                   </span>
                   <span
                   <span
@@ -377,7 +371,7 @@
                     v-if="createRuleObject.actions.length > 1"
                     v-if="createRuleObject.actions.length > 1"
                     name="trash"
                     name="trash"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
-                    @click.native="deleteAction(createRuleObject, key)"
+                    @click="deleteAction(createRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
               </div>
               </div>
@@ -449,16 +443,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="editRuleModalOpen" @close="closeEditModal" :overflow="true">
     <Modal :open="editRuleModalOpen" @close="closeEditModal" :overflow="true">
-      <div class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6 my-12">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Edit rule
-        </h2>
+      <template v-slot:title> Edit rule </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Rules work on all emails, including replies and also send froms. New conditions and
           Rules work on all emails, including replies and also send froms. New conditions and
           actions will be added over time.
           actions will be added over time.
@@ -603,7 +593,7 @@
                     v-if="editRuleObject.conditions.length > 1"
                     v-if="editRuleObject.conditions.length > 1"
                     name="trash"
                     name="trash"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
-                    @click.native="deleteCondition(editRuleObject, key)"
+                    @click="deleteCondition(editRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
               </div>
               </div>
@@ -614,7 +604,7 @@
                     <icon
                     <icon
                       name="close"
                       name="close"
                       class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
                       class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
-                      @click.native="editRuleObject.conditions[key].values.splice(index, 1)"
+                      @click="editRuleObject.conditions[key].values.splice(index, 1)"
                     />
                     />
                   </span>
                   </span>
                   <span
                   <span
@@ -740,7 +730,7 @@
                     v-if="editRuleObject.actions.length > 1"
                     v-if="editRuleObject.actions.length > 1"
                     name="trash"
                     name="trash"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
                     class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
-                    @click.native="deleteAction(editRuleObject, key)"
+                    @click="deleteAction(editRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
               </div>
               </div>
@@ -812,16 +802,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteRuleModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteRuleModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete rule
-        </h2>
+      <template v-slot:title> Delete rule </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this rule?</p>
         <p class="mt-4 text-grey-700">Are you sure you want to delete this rule?</p>
         <div class="mt-6">
         <div class="mt-6">
           <button
           <button
@@ -841,7 +827,7 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -945,14 +931,6 @@ export default {
     activeRules() {
     activeRules() {
       return _.filter(this.rows, rule => rule.active)
       return _.filter(this.rows, rule => rule.active)
     },
     },
-    dragOptions() {
-      return {
-        animation: 0,
-        group: 'description',
-        disabled: false,
-        ghostClass: 'ghost',
-      }
-    },
     rowsIds() {
     rowsIds() {
       return _.map(this.rows, row => row.id)
       return _.map(this.rows, row => row.id)
     },
     },

+ 42 - 50
resources/js/pages/Usernames.vue

@@ -12,7 +12,7 @@
         />
         />
         <icon
         <icon
           v-if="search"
           v-if="search"
-          @click.native="search = ''"
+          @click="search = ''"
           name="close-circle"
           name="close-circle"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
         />
         />
@@ -34,7 +34,7 @@
 
 
     <vue-good-table
     <vue-good-table
       v-if="initialUsernames.length"
       v-if="initialUsernames.length"
-      @on-search="debounceToolips"
+      v-on:search="debounceToolips"
       :columns="columns"
       :columns="columns"
       :rows="rows"
       :rows="rows"
       :search-options="{
       :search-options="{
@@ -48,15 +48,15 @@
       }"
       }"
       styleClass="vgt-table"
       styleClass="vgt-table"
     >
     >
-      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+      <template #emptystate class="flex items-center justify-center h-24 text-lg text-grey-700">
         No usernames found for that search!
         No usernames found for that search!
-      </div>
-      <template slot="table-row" slot-scope="props">
+      </template>
+      <template #table-row="props">
         <span
         <span
           v-if="props.column.field == 'created_at'"
           v-if="props.column.field == 'created_at'"
           class="tooltip outline-none text-sm"
           class="tooltip outline-none text-sm"
-          :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
-          >{{ props.row.created_at | timeAgo }}
+          :data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
+          >{{ $filters.timeAgo(props.row.created_at) }}
         </span>
         </span>
         <span v-else-if="props.column.field == 'username'">
         <span v-else-if="props.column.field == 'username'">
           <span
           <span
@@ -65,7 +65,7 @@
             v-clipboard="() => rows[props.row.originalIndex].username"
             v-clipboard="() => rows[props.row.originalIndex].username"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:success="clipboardSuccess"
             v-clipboard:error="clipboardError"
             v-clipboard:error="clipboardError"
-            >{{ props.row.username | truncate(30) }}</span
+            >{{ $filters.truncate(props.row.username, 30) }}</span
           >
           >
           <span
           <span
             v-if="isDefault(props.row.id)"
             v-if="isDefault(props.row.id)"
@@ -92,20 +92,20 @@
             <icon
             <icon
               name="close"
               name="close"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
-              @click.native="usernameIdToEdit = usernameDescriptionToEdit = ''"
+              @click="usernameIdToEdit = usernameDescriptionToEdit = ''"
             />
             />
             <icon
             <icon
               name="save"
               name="save"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
               class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
-              @click.native="editUsername(rows[props.row.originalIndex])"
+              @click="editUsername(rows[props.row.originalIndex])"
             />
             />
           </div>
           </div>
           <div v-else-if="props.row.description" class="flex items-centers">
           <div v-else-if="props.row.description" class="flex items-centers">
-            <span class="outline-none">{{ props.row.description | truncate(60) }}</span>
+            <span class="outline-none">{{ $filters.truncate(props.row.description, 60) }}</span>
             <icon
             <icon
               name="edit"
               name="edit"
               class="inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer ml-2"
               class="inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer ml-2"
-              @click.native="
+              @click="
                 ;(usernameIdToEdit = props.row.id),
                 ;(usernameIdToEdit = props.row.id),
                   (usernameDescriptionToEdit = props.row.description)
                   (usernameDescriptionToEdit = props.row.description)
               "
               "
@@ -115,24 +115,24 @@
             <icon
             <icon
               name="plus"
               name="plus"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native=";(usernameIdToEdit = props.row.id), (usernameDescriptionToEdit = '')"
+              @click=";(usernameIdToEdit = props.row.id), (usernameDescriptionToEdit = '')"
             />
             />
           </div>
           </div>
         </span>
         </span>
         <span v-else-if="props.column.field === 'default_recipient'">
         <span v-else-if="props.column.field === 'default_recipient'">
           <div v-if="props.row.default_recipient">
           <div v-if="props.row.default_recipient">
-            {{ props.row.default_recipient.email | truncate(30) }}
+            {{ $filters.truncate(props.row.default_recipient.email, 30) }}
             <icon
             <icon
               name="edit"
               name="edit"
               class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="ml-2 inline-block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native="openUsernameDefaultRecipientModal(props.row)"
+              @click="openUsernameDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
           <div class="flex justify-center" v-else>
           <div class="flex justify-center" v-else>
             <icon
             <icon
               name="plus"
               name="plus"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-              @click.native="openUsernameDefaultRecipientModal(props.row)"
+              @click="openUsernameDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
         </span>
         </span>
@@ -158,7 +158,7 @@
             v-if="!isDefault(props.row.id)"
             v-if="!isDefault(props.row.id)"
             name="trash"
             name="trash"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
-            @click.native="openDeleteModal(props.row.id)"
+            @click="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>
       </template>
       </template>
@@ -187,12 +187,8 @@
     </div>
     </div>
 
 
     <Modal :open="addUsernameModalOpen" @close="addUsernameModalOpen = false">
     <Modal :open="addUsernameModalOpen" @close="addUsernameModalOpen = false">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Add new username
-        </h2>
+      <template v-slot:title> Add new username </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Please choose usernames carefully as you can only add a maximum of
           Please choose usernames carefully as you can only add a maximum of
           {{ usernameCount }}. You can login with any of your usernames.
           {{ usernameCount }}. You can login with any of your usernames.
@@ -225,25 +221,22 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="usernameDefaultRecipientModalOpen" @close="closeUsernameDefaultRecipientModal">
     <Modal :open="usernameDefaultRecipientModalOpen" @close="closeUsernameDefaultRecipientModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Update Default Recipient
-        </h2>
+      <template v-slot:title> Update Default Recipient </template>
+      <template v-slot:content>
         <p class="my-4 text-grey-700">
         <p class="my-4 text-grey-700">
           Select the default recipient for this username. This overrides the default recipient in
           Select the default recipient for this username. This overrides the default recipient in
           your account settings. Leave it empty if you would like to use the default recipient in
           your account settings. Leave it empty if you would like to use the default recipient in
           your account settings.
           your account settings.
         </p>
         </p>
         <multiselect
         <multiselect
-          v-model="defaultRecipient"
+          v-model="defaultRecipientId"
           :options="recipientOptions"
           :options="recipientOptions"
-          :multiple="false"
+          mode="single"
+          value-prop="id"
           :close-on-select="true"
           :close-on-select="true"
           :clear-on-select="false"
           :clear-on-select="false"
           :searchable="false"
           :searchable="false"
@@ -251,8 +244,6 @@
           placeholder="Select recipient"
           placeholder="Select recipient"
           label="email"
           label="email"
           track-by="email"
           track-by="email"
-          :preselect-first="false"
-          :show-labels="false"
         >
         >
         </multiselect>
         </multiselect>
         <div class="mt-6">
         <div class="mt-6">
@@ -273,16 +264,12 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
 
 
     <Modal :open="deleteUsernameModalOpen" @close="closeDeleteModal">
     <Modal :open="deleteUsernameModalOpen" @close="closeDeleteModal">
-      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
-        <h2
-          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
-        >
-          Delete username
-        </h2>
+      <template v-slot:title> Delete username </template>
+      <template v-slot:content>
         <p class="mt-4 text-grey-700">
         <p class="mt-4 text-grey-700">
           Are you sure you want to delete this username? This will also delete all aliases
           Are you sure you want to delete this username? This will also delete all aliases
           associated with this username. You will no longer be able to receive any emails at this
           associated with this username. You will no longer be able to receive any emails at this
@@ -307,7 +294,7 @@
             Cancel
             Cancel
           </button>
           </button>
         </div>
         </div>
-      </div>
+      </template>
     </Modal>
     </Modal>
   </div>
   </div>
 </template>
 </template>
@@ -319,7 +306,7 @@ import { roundArrow } from 'tippy.js'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/svg-arrow.css'
 import 'tippy.js/dist/tippy.css'
 import 'tippy.js/dist/tippy.css'
 import tippy from 'tippy.js'
 import tippy from 'tippy.js'
-import Multiselect from 'vue-multiselect'
+import Multiselect from '@vueform/multiselect'
 
 
 export default {
 export default {
   props: {
   props: {
@@ -358,7 +345,7 @@ export default {
       deleteUsernameModalOpen: false,
       deleteUsernameModalOpen: false,
       usernameDefaultRecipientModalOpen: false,
       usernameDefaultRecipientModalOpen: false,
       defaultRecipientUsernameToEdit: {},
       defaultRecipientUsernameToEdit: {},
-      defaultRecipient: {},
+      defaultRecipientId: null,
       editDefaultRecipientLoading: false,
       editDefaultRecipientLoading: false,
       errors: {},
       errors: {},
       columns: [
       columns: [
@@ -492,12 +479,12 @@ export default {
     openUsernameDefaultRecipientModal(username) {
     openUsernameDefaultRecipientModal(username) {
       this.usernameDefaultRecipientModalOpen = true
       this.usernameDefaultRecipientModalOpen = true
       this.defaultRecipientUsernameToEdit = username
       this.defaultRecipientUsernameToEdit = username
-      this.defaultRecipient = username.default_recipient
+      this.defaultRecipientId = username.default_recipient_id
     },
     },
     closeUsernameDefaultRecipientModal() {
     closeUsernameDefaultRecipientModal() {
       this.usernameDefaultRecipientModalOpen = false
       this.usernameDefaultRecipientModalOpen = false
       this.defaultRecipientUsernameToEdit = {}
       this.defaultRecipientUsernameToEdit = {}
-      this.defaultRecipient = {}
+      this.defaultRecipientId = null
     },
     },
     editUsername(username) {
     editUsername(username) {
       if (this.usernameDescriptionToEdit.length > 200) {
       if (this.usernameDescriptionToEdit.length > 200) {
@@ -532,7 +519,7 @@ export default {
         .patch(
         .patch(
           `/api/v1/usernames/${this.defaultRecipientUsernameToEdit.id}/default-recipient`,
           `/api/v1/usernames/${this.defaultRecipientUsernameToEdit.id}/default-recipient`,
           JSON.stringify({
           JSON.stringify({
-            default_recipient: this.defaultRecipient ? this.defaultRecipient.id : '',
+            default_recipient: this.defaultRecipientId,
           }),
           }),
           {
           {
             headers: { 'Content-Type': 'application/json' },
             headers: { 'Content-Type': 'application/json' },
@@ -540,16 +527,21 @@ export default {
         )
         )
         .then(response => {
         .then(response => {
           let username = _.find(this.rows, ['id', this.defaultRecipientUsernameToEdit.id])
           let username = _.find(this.rows, ['id', this.defaultRecipientUsernameToEdit.id])
-          username.default_recipient = this.defaultRecipient
+          username.default_recipient = _.find(this.recipientOptions, [
+            'id',
+            this.defaultRecipientId,
+          ])
+          username.default_recipient_id = this.defaultRecipientId
+
           this.usernameDefaultRecipientModalOpen = false
           this.usernameDefaultRecipientModalOpen = false
           this.editDefaultRecipientLoading = false
           this.editDefaultRecipientLoading = false
-          this.defaultRecipient = {}
+          this.defaultRecipientId = null
           this.success("Username's default recipient updated")
           this.success("Username's default recipient updated")
         })
         })
         .catch(error => {
         .catch(error => {
           this.usernameDefaultRecipientModalOpen = false
           this.usernameDefaultRecipientModalOpen = false
           this.editDefaultRecipientLoading = false
           this.editDefaultRecipientLoading = false
-          this.defaultRecipient = {}
+          this.defaultRecipientId = null
           this.error()
           this.error()
         })
         })
     },
     },

+ 5 - 0
resources/views/auth/login.blade.php

@@ -41,6 +41,11 @@
                                     {{ $errors->first('username') }}
                                     {{ $errors->first('username') }}
                                 </p>
                                 </p>
                             @endif
                             @endif
+                            @if ($errors->has('id'))
+                                <p class="text-red-500 text-xs italic mt-4">
+                                    {{ $errors->first('id') }}
+                                </p>
+                            @endif
                         </div>
                         </div>
 
 
                         <div class="flex flex-wrap mb-2">
                         <div class="flex flex-wrap mb-2">

+ 1 - 1
resources/views/domains/index.blade.php

@@ -4,6 +4,6 @@
     <div class="container py-8">
     <div class="container py-8">
         @include('shared.status')
         @include('shared.status')
 
 
-        <domains :initial-domains="{{json_encode($domains)}}" domain-name="{{config('anonaddy.domain')}}" hostname="{{config('anonaddy.hostname')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients) }}" aa-verify="{{ sha1(config('anonaddy.secret') . Auth::user()->id . Auth::user()->domains->count()) }}" />
+        <domains :initial-domains="{{json_encode($domains)}}" domain-name="{{config('anonaddy.domain')}}" hostname="{{config('anonaddy.hostname')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients()->select(['id', 'email'])->get()) }}" aa-verify="{{ sha1(config('anonaddy.secret') . Auth::user()->id . Auth::user()->domains->count()) }}" />
     </div>
     </div>
 @endsection
 @endsection

+ 0 - 1
resources/views/layouts/app.blade.php

@@ -27,7 +27,6 @@
 
 
         @yield('content')
         @yield('content')
 
 
-        <portal-target name="modals"></portal-target>
         <notifications position="bottom right" />
         <notifications position="bottom right" />
     </div>
     </div>
 
 

+ 1 - 1
resources/views/usernames/index.blade.php

@@ -4,6 +4,6 @@
     <div class="container py-8">
     <div class="container py-8">
         @include('shared.status')
         @include('shared.status')
 
 
-        <usernames :user="{{json_encode(Auth::user())}}" :initial-usernames="{{json_encode($usernames)}}" :username-count="{{config('anonaddy.additional_username_limit')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients) }}" />
+        <usernames :user="{{json_encode(Auth::user())}}" :initial-usernames="{{json_encode($usernames)}}" :username-count="{{config('anonaddy.additional_username_limit')}}" :recipient-options="{{ json_encode(Auth::user()->verifiedRecipients()->select(['id', 'email'])->get()) }}" />
     </div>
     </div>
 @endsection
 @endsection

+ 3 - 0
routes/api.php

@@ -8,6 +8,7 @@ use App\Http\Controllers\Api\ActiveUsernameController;
 use App\Http\Controllers\Api\AliasController;
 use App\Http\Controllers\Api\AliasController;
 use App\Http\Controllers\Api\AliasRecipientController;
 use App\Http\Controllers\Api\AliasRecipientController;
 use App\Http\Controllers\Api\AllowedRecipientController;
 use App\Http\Controllers\Api\AllowedRecipientController;
+use App\Http\Controllers\Api\ApiTokenDetailController;
 use App\Http\Controllers\Api\AppVersionController;
 use App\Http\Controllers\Api\AppVersionController;
 use App\Http\Controllers\Api\CatchAllDomainController;
 use App\Http\Controllers\Api\CatchAllDomainController;
 use App\Http\Controllers\Api\CatchAllUsernameController;
 use App\Http\Controllers\Api\CatchAllUsernameController;
@@ -156,4 +157,6 @@ Route::group([
     Route::get('/account-details', [AccountDetailController::class, 'index']);
     Route::get('/account-details', [AccountDetailController::class, 'index']);
 
 
     Route::get('/app-version', [AppVersionController::class, 'index']);
     Route::get('/app-version', [AppVersionController::class, 'index']);
+
+    Route::get('api-token-details', [ApiTokenDetailController::class, 'show']);
 });
 });

+ 5 - 1
routes/web.php

@@ -1,6 +1,7 @@
 <?php
 <?php
 
 
 use App\Http\Controllers\AliasExportController;
 use App\Http\Controllers\AliasExportController;
+use App\Http\Controllers\Auth\ApiAuthenticationController;
 use App\Http\Controllers\Auth\BackupCodeController;
 use App\Http\Controllers\Auth\BackupCodeController;
 use App\Http\Controllers\Auth\ForgotUsernameController;
 use App\Http\Controllers\Auth\ForgotUsernameController;
 use App\Http\Controllers\Auth\PersonalAccessTokenController;
 use App\Http\Controllers\Auth\PersonalAccessTokenController;
@@ -42,13 +43,16 @@ use Illuminate\Support\Facades\Route;
 
 
 Auth::routes(['verify' => true, 'register' => config('anonaddy.enable_registration')]);
 Auth::routes(['verify' => true, 'register' => config('anonaddy.enable_registration')]);
 
 
+// Get API access token
+Route::post('api/auth/login', [ApiAuthenticationController::class, 'login']);
+Route::post('api/auth/mfa', [ApiAuthenticationController::class, 'mfa']);
 
 
 Route::controller(ForgotUsernameController::class)->group(function () {
 Route::controller(ForgotUsernameController::class)->group(function () {
     Route::get('/username/reminder', 'show')->name('username.reminder.show');
     Route::get('/username/reminder', 'show')->name('username.reminder.show');
     Route::post('/username/email', 'sendReminderEmail')->name('username.email');
     Route::post('/username/email', 'sendReminderEmail')->name('username.email');
 });
 });
 
 
-Route::post('/login/2fa', [TwoFactorAuthController::class, 'authenticateTwoFactor'])->name('login.2fa')->middleware(['2fa', 'throttle', 'auth']);
+Route::post('/login/2fa', [TwoFactorAuthController::class, 'authenticateTwoFactor'])->name('login.2fa')->middleware(['2fa', 'throttle:3,1', 'auth']);
 
 
 Route::controller(BackupCodeController::class)->group(function () {
 Route::controller(BackupCodeController::class)->group(function () {
     Route::get('/login/backup-code', 'index')->name('login.backup_code.index');
     Route::get('/login/backup-code', 'index')->name('login.backup_code.index');

+ 0 - 4
tests/Feature/Api/AliasesTest.php

@@ -297,8 +297,6 @@ class AliasesTest extends TestCase
     /** @test */
     /** @test */
     public function user_can_forget_alias()
     public function user_can_forget_alias()
     {
     {
-        $this->withoutExceptionHandling();
-
         $alias = Alias::factory()->create([
         $alias = Alias::factory()->create([
             'user_id' => $this->user->id
             'user_id' => $this->user->id
         ]);
         ]);
@@ -316,8 +314,6 @@ class AliasesTest extends TestCase
     /** @test */
     /** @test */
     public function user_can_forget_shared_domain_alias()
     public function user_can_forget_shared_domain_alias()
     {
     {
-        $this->withoutExceptionHandling();
-
         $sharedDomainAlias = Alias::factory()->create([
         $sharedDomainAlias = Alias::factory()->create([
             'user_id' => $this->user->id,
             'user_id' => $this->user->id,
             'domain' => 'anonaddy.me',
             'domain' => 'anonaddy.me',

+ 34 - 0
tests/Feature/Api/ApiTokenDetailsTest.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Tests\Feature\Api;
+
+use App\Models\User;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Tests\TestCase;
+
+class ApiTokenDetailsTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+    }
+
+    /** @test */
+    public function user_can_get_account_details()
+    {
+        $user = User::factory()->create();
+        $token = $user->createToken('New');
+
+        $response = $this->withHeaders([
+            'Authorization' => 'Bearer ' . $token->plainTextToken,
+        ])->json('GET', '/api/v1/api-token-details');
+
+        $response->assertSuccessful();
+
+        $this->assertEquals($token->accessToken->name, $response->json()['name']);
+        $this->assertEquals($token->accessToken->created_at, $response->json()['created_at']);
+        $this->assertEquals($token->accessToken->expires_at, $response->json()['expires_at']);
+    }
+}

+ 178 - 0
tests/Feature/ApiAuthenticationTest.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Models\User;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
+use Illuminate\Support\Facades\Hash;
+use Tests\TestCase;
+
+class ApiAuthenticationTest extends TestCase
+{
+    use RefreshDatabase;
+
+    protected $user;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->user = User::factory()->create([
+            'password' => Hash::make('mypassword')
+        ]);
+
+        $this->user->usernames()->save($this->user->defaultUsername);
+        $this->user->defaultUsername->username = 'johndoe';
+        $this->user->defaultUsername->save();
+    }
+
+    /** @test */
+    public function user_can_retreive_valid_access_token()
+    {
+        $this->withoutMiddleware(ThrottleRequestsWithRedis::class);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'mypassword',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertSuccessful();
+        $this->assertEquals($this->user->tokens[0]->token, hash('sha256', $response->json()['api_key']));
+    }
+
+    /** @test */
+    public function user_password_must_be_correct_to_get_access_token()
+    {
+        $this->withoutMiddleware(ThrottleRequestsWithRedis::class);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'myincorrectpassword',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertUnauthorized();
+    }
+
+    /** @test */
+    public function user_must_exist_to_get_access_token()
+    {
+        $this->withoutMiddleware(ThrottleRequestsWithRedis::class);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'doesnotexist',
+            'password' => 'mypassword',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertUnauthorized();
+        $response->assertExactJson(['error' => 'The provided credentials are incorrect']);
+    }
+
+    /** @test */
+    public function user_is_throttled_by_middleware_for_too_many_requests()
+    {
+        $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'incorrect1',
+            'device_name' => 'Firefox'
+        ]);
+
+        $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'incorrect2',
+            'device_name' => 'Firefox'
+        ]);
+
+        $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'incorrect3',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'incorrect4',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertStatus(429);
+    }
+
+    /** @test */
+    public function user_cannot_get_access_token_with_webauthn_enabled()
+    {
+        $this->withoutMiddleware(ThrottleRequestsWithRedis::class);
+
+        $this->user->webauthnKeys()->create([
+            'name' => 'key',
+            'enabled' => true,
+            'credentialId' => 'xyz',
+            'type' => 'public-key',
+            'transports' => [],
+            'attestationType' => 'none',
+            'trustPath' => '{"type":"Webauthn\\TrustPath\\EmptyTrustPath"}',
+            'aaguid' => '00000000-0000-0000-0000-000000000000',
+            'credentialPublicKey' => 'xyz',
+            'counter' => 0
+        ]);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'mypassword',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertForbidden();
+        $response->assertExactJson(['error' => 'WebAuthn authentication is not currently supported from the extension or mobile apps, please use an API key to login instead']);
+    }
+
+    /** @test */
+    public function user_must_provide_correct_otp_if_enabled()
+    {
+        $this->withoutMiddleware(ThrottleRequestsWithRedis::class);
+
+        $secret = app('pragmarx.google2fa')->generateSecretKey();
+        $this->user->update([
+            'two_factor_secret' => $secret,
+            'two_factor_enabled' => true
+        ]);
+
+        $response = $this->json('POST', '/api/auth/login', [
+            'username' => 'johndoe',
+            'password' => 'mypassword',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response->assertStatus(422);
+
+        $mfaKey = $response->json()['mfa_key'];
+        $csrfToken = $response->json()['csrf_token'];
+        $this->assertNotNull($mfaKey);
+        $this->assertNotNull($csrfToken);
+
+        $response2 = $this->withHeaders([
+            'X-CSRF-TOKEN' => $csrfToken,
+        ])->json('POST', '/api/auth/mfa', [
+            'mfa_key' => $mfaKey,
+            'otp' => '000000',
+            'device_name' => 'Firefox'
+        ]);
+
+        $response2->assertUnauthorized();
+        $response2->assertExactJson(['error' => 'The \'One Time Password\' typed was wrong']);
+
+        $response3 = $this->withHeaders([
+            'X-CSRF-TOKEN' => $csrfToken,
+        ])->json('POST', '/api/auth/mfa', [
+            'mfa_key' => $mfaKey,
+            'otp' => app('pragmarx.google2fa')->getCurrentOtp($secret),
+            'device_name' => 'Firefox'
+        ]);
+
+        $response3->assertSuccessful();
+        $this->assertEquals($this->user->tokens[0]->token, hash('sha256', $response3->json()['api_key']));
+    }
+}

+ 4 - 0
tests/Feature/LoginTest.php

@@ -99,6 +99,8 @@ class LoginTest extends TestCase
     /** @test */
     /** @test */
     public function user_can_receive_username_reminder_email()
     public function user_can_receive_username_reminder_email()
     {
     {
+        $this->withoutMiddleware();
+
         Notification::fake();
         Notification::fake();
 
 
         $recipient = $this->user->recipients[0];
         $recipient = $this->user->recipients[0];
@@ -116,6 +118,8 @@ class LoginTest extends TestCase
     /** @test */
     /** @test */
     public function username_reminder_email_not_sent_for_unkown_email()
     public function username_reminder_email_not_sent_for_unkown_email()
     {
     {
+        $this->withoutMiddleware();
+
         Notification::fake();
         Notification::fake();
 
 
         $this->post('/username/email', [
         $this->post('/username/email', [

+ 0 - 1
tests/Feature/ShowFailedDeliveriesTest.php

@@ -28,7 +28,6 @@ class ShowFailedDeliveriesTest extends TestCase
     /** @test */
     /** @test */
     public function user_can_view_failed_deliveries_from_the_failed_deliveries_page()
     public function user_can_view_failed_deliveries_from_the_failed_deliveries_page()
     {
     {
-        $this->withoutExceptionHandling();
         $failedDeliveries = FailedDelivery::factory()->count(3)->create([
         $failedDeliveries = FailedDelivery::factory()->count(3)->create([
             'user_id' => $this->user->id
             'user_id' => $this->user->id
         ]);
         ]);

+ 0 - 1
tests/Feature/ShowRecipientsTest.php

@@ -32,7 +32,6 @@ class ShowRecipientsTest extends TestCase
     /** @test */
     /** @test */
     public function user_can_view_recipients_from_the_recipients_page()
     public function user_can_view_recipients_from_the_recipients_page()
     {
     {
-        $this->withoutExceptionHandling();
         $recipients = Recipient::factory()->count(5)->create([
         $recipients = Recipient::factory()->count(5)->create([
             'user_id' => $this->user->id
             'user_id' => $this->user->id
         ]);
         ]);

部分文件因为文件数量过多而无法显示