Explorar o código

Add support for U2F devices

Will Browning %!s(int64=4) %!d(string=hai) anos
pai
achega
f1d5963dfd
Modificáronse 39 ficheiros con 2260 adicións e 399 borrados
  1. 31 0
      app/Facades/Webauthn.php
  2. 4 1
      app/Http/Controllers/Auth/BackupCodeController.php
  3. 2 0
      app/Http/Controllers/Auth/TwoFactorAuthController.php
  4. 134 0
      app/Http/Controllers/Auth/WebauthnController.php
  5. 16 0
      app/Http/Controllers/DefaultAliasFormatController.php
  6. 1 0
      app/Http/Kernel.php
  7. 8 0
      app/Models/User.php
  8. 15 0
      app/Models/WebauthnKey.php
  9. 49 0
      app/Services/Webauthn.php
  10. 1 0
      composer.json
  11. 730 54
      composer.lock
  12. 2 2
      config/version.yml
  13. 225 0
      config/webauthn.php
  14. 0 0
      database/factories/DeletedUsernameFactory.php
  15. 44 0
      database/migrations/2020_11_24_120152_create_webauthn_keys_table.php
  16. 163 204
      package-lock.json
  17. 2 2
      package.json
  18. 1 0
      resources/js/app.js
  19. 127 0
      resources/js/components/WebauthnKeys.vue
  20. 12 12
      resources/js/pages/Aliases.vue
  21. 5 5
      resources/js/pages/Domains.vue
  22. 4 4
      resources/js/pages/Recipients.vue
  23. 8 8
      resources/js/pages/Rules.vue
  24. 5 5
      resources/js/pages/Usernames.vue
  25. 239 0
      resources/js/webauthn.js
  26. 1 1
      resources/views/auth/backup_code.blade.php
  27. 1 1
      resources/views/auth/login.blade.php
  28. 1 1
      resources/views/auth/passwords/email.blade.php
  29. 1 1
      resources/views/auth/passwords/reset.blade.php
  30. 1 1
      resources/views/auth/register.blade.php
  31. 1 1
      resources/views/auth/two_factor.blade.php
  32. 1 1
      resources/views/auth/usernames/email.blade.php
  33. 27 0
      resources/views/layouts/auth.blade.php
  34. 50 52
      resources/views/nav/nav.blade.php
  35. 92 41
      resources/views/settings/show.blade.php
  36. 108 0
      resources/views/vendor/webauthn/authenticate.blade.php
  37. 135 0
      resources/views/vendor/webauthn/register.blade.php
  38. 12 2
      routes/web.php
  39. 1 0
      webpack.mix.js

+ 31 - 0
app/Facades/Webauthn.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Facades;
+
+use Illuminate\Support\Facades\Facade;
+use Webauthn\PublicKeyCredentialCreationOptions;
+use Webauthn\PublicKeyCredentialRequestOptions;
+
+/**
+ * @method static PublicKeyCredentialCreationOptions getRegisterData(\Illuminate\Contracts\Auth\Authenticatable $user)
+ * @method static \LaravelWebauthn\Models\WebauthnKey doRegister(\Illuminate\Contracts\Auth\Authenticatable $user, PublicKeyCredentialCreationOptions $publicKey, string $data, string $keyName)
+ * @method static PublicKeyCredentialRequestOptions getAuthenticateData(\Illuminate\Contracts\Auth\Authenticatable $user)
+ * @method static bool doAuthenticate(\Illuminate\Contracts\Auth\Authenticatable $user, PublicKeyCredentialRequestOptions $publicKey, string $data)
+ * @method static void forceAuthenticate()
+ * @method static bool check()
+ * @method static bool enabled(\Illuminate\Contracts\Auth\Authenticatable $user)
+ *
+ * @see \LaravelWebauthn\Webauthn
+ */
+class Webauthn extends Facade
+{
+    /**
+     * Get the registered name of the component.
+     *
+     * @return string
+     */
+    protected static function getFacadeAccessor()
+    {
+        return \App\Services\Webauthn::class;
+    }
+}

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

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Controllers\Auth;
 namespace App\Http\Controllers\Auth;
 
 
+use App\Facades\Webauthn;
 use App\Http\Controllers\Controller;
 use App\Http\Controllers\Controller;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Hash;
@@ -19,7 +20,7 @@ class BackupCodeController extends Controller
     {
     {
         $authenticator = app(Authenticator::class)->boot($request);
         $authenticator = app(Authenticator::class)->boot($request);
 
 
-        if ($authenticator->isAuthenticated() || ! $request->user()->two_factor_enabled) {
+        if (($authenticator->isAuthenticated() || ! $request->user()->two_factor_enabled) && ! Webauthn::enabled($request->user())) {
             return redirect('/');
             return redirect('/');
         }
         }
 
 
@@ -46,6 +47,8 @@ class BackupCodeController extends Controller
             'two_factor_backup_code' => null
             'two_factor_backup_code' => null
         ]);
         ]);
 
 
+        user()->webauthnKeys()->delete();
+
         if ($request->session()->has('intended_path')) {
         if ($request->session()->has('intended_path')) {
             return redirect($request->session()->pull('intended_path'));
             return redirect($request->session()->pull('intended_path'));
         }
         }

+ 2 - 0
app/Http/Controllers/Auth/TwoFactorAuthController.php

@@ -26,6 +26,8 @@ class TwoFactorAuthController extends Controller
             return redirect(url()->previous().'#two-factor')->withErrors(['two_factor_token' => 'The token you entered was incorrect']);
             return redirect(url()->previous().'#two-factor')->withErrors(['two_factor_token' => 'The token you entered was incorrect']);
         }
         }
 
 
+        user()->webauthnKeys()->delete();
+
         user()->update([
         user()->update([
             'two_factor_enabled' => true,
             'two_factor_enabled' => true,
             'two_factor_backup_code' => bcrypt($code = Str::random(40))
             'two_factor_backup_code' => bcrypt($code = Str::random(40))

+ 134 - 0
app/Http/Controllers/Auth/WebauthnController.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace App\Http\Controllers\Auth;
+
+use App\Facades\Webauthn;
+use App\Models\WebauthnKey;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Redirect;
+use Illuminate\Support\Facades\Response;
+use Illuminate\Support\Str;
+use LaravelWebauthn\Http\Controllers\WebauthnController as ControllersWebauthnController;
+use Webauthn\PublicKeyCredentialCreationOptions;
+
+class WebauthnController extends ControllersWebauthnController
+{
+    public function index()
+    {
+        return user()->webauthnKeys()->latest()->select(['id','name','created_at'])->get()->values();
+    }
+
+    /**
+     * PublicKey Creation session name.
+     *
+     * @var string
+     */
+    private const SESSION_PUBLICKEY_CREATION = 'webauthn.publicKeyCreation';
+
+    /**
+     * Validate and create the Webauthn request.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
+     */
+    public function create(Request $request)
+    {
+        $request->validate([
+            'name' => 'required|max:50',
+            'register' => 'required',
+        ]);
+
+        try {
+            $publicKey = $request->session()->pull(self::SESSION_PUBLICKEY_CREATION);
+            if (! $publicKey instanceof PublicKeyCredentialCreationOptions) {
+                throw new ModelNotFoundException(trans('webauthn::errors.create_data_not_found'));
+            }
+
+            $webauthnKey = Webauthn::doRegister(
+                $request->user(),
+                $publicKey,
+                $this->input($request, 'register'),
+                $this->input($request, 'name')
+            );
+
+            user()->update([
+                'two_factor_enabled' => false
+            ]);
+
+            return $this->redirectAfterSuccessRegister($webauthnKey);
+        } catch (\Exception $e) {
+            return Response::json([
+                'error' => [
+                    'message' => $e->getMessage(),
+                ],
+            ], 403);
+        }
+    }
+
+    /**
+     * Return the redirect destination after a successfull register.
+     *
+     * @param WebauthnKey $webauthnKey
+     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
+     */
+    protected function redirectAfterSuccessRegister($webauthnKey)
+    {
+        if ($this->config->get('webauthn.register.postSuccessRedirectRoute', '') !== '') {
+            user()->update([
+                'two_factor_backup_code' => bcrypt($code = Str::random(40))
+            ]);
+
+            return Redirect::intended($this->config->get('webauthn.register.postSuccessRedirectRoute'))->with(['backupCode' => $code]);
+        } else {
+            return Response::json([
+                'result' => true,
+                'id' => $webauthnKey->id,
+                'object' => 'webauthnKey',
+                'name' => $webauthnKey->name,
+                'counter' => $webauthnKey->counter,
+            ], 201);
+        }
+    }
+
+    /**
+     * Remove an existing Webauthn key.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function destroy(Request $request, $webauthnKeyId)
+    {
+        try {
+            user()->webauthnKeys()
+                ->findOrFail($webauthnKeyId)
+                ->delete();
+
+            return Response::json([
+                'deleted' => true,
+                'id' => $webauthnKeyId,
+            ]);
+        } catch (ModelNotFoundException $e) {
+            return Response::json([
+                'error' => [
+                    'message' => trans('webauthn::errors.object_not_found'),
+                ],
+            ], 404);
+        }
+    }
+
+    /**
+     * Retrieve the input with a string result.
+     *
+     * @param \Illuminate\Http\Request $request
+     * @param string $name
+     * @param string $default
+     * @return string
+     */
+    private function input(Request $request, string $name, string $default = ''): string
+    {
+        $result = $request->input($name);
+
+        return is_string($result) ? $result : $default;
+    }
+}

+ 16 - 0
app/Http/Controllers/DefaultAliasFormatController.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\UpdateDefaultAliasFormatRequest;
+
+class DefaultAliasFormatController extends Controller
+{
+    public function update(UpdateDefaultAliasFormatRequest $request)
+    {
+        user()->default_alias_format = $request->format;
+        user()->save();
+
+        return back()->with(['status' => 'Default Alias Format Updated Successfully']);
+    }
+}

+ 1 - 0
app/Http/Kernel.php

@@ -64,5 +64,6 @@ class Kernel extends HttpKernel
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::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' => \LaravelWebauthn\Http\Middleware\WebauthnMiddleware::class,
     ];
     ];
 }
 }

+ 8 - 0
app/Models/User.php

@@ -189,6 +189,14 @@ class User extends Authenticatable implements MustVerifyEmail
         return $this->hasMany(AdditionalUsername::class);
         return $this->hasMany(AdditionalUsername::class);
     }
     }
 
 
+    /**
+     * Get all of the user's webauthn keys.
+     */
+    public function webauthnKeys()
+    {
+        return $this->hasMany(WebauthnKey::class);
+    }
+
     /**
     /**
      * Get all of the user's verified recipients.
      * Get all of the user's verified recipients.
      */
      */

+ 15 - 0
app/Models/WebauthnKey.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use App\Traits\HasUuid;
+use LaravelWebauthn\Models\WebauthnKey as ModelsWebauthnKey;
+
+class WebauthnKey extends ModelsWebauthnKey
+{
+    use HasUuid;
+
+    public $incrementing = false;
+
+    protected $keyType = 'string';
+}

+ 49 - 0
app/Services/Webauthn.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\WebauthnKey;
+use Illuminate\Contracts\Auth\Authenticatable as User;
+use LaravelWebauthn\Events\WebauthnRegister;
+use LaravelWebauthn\Services\Webauthn as ServicesWebauthn;
+use LaravelWebauthn\Services\Webauthn\PublicKeyCredentialValidator;
+use Webauthn\PublicKeyCredentialCreationOptions;
+use Webauthn\PublicKeyCredentialSource;
+
+class Webauthn extends ServicesWebauthn
+{
+    public function doRegister(User $user, PublicKeyCredentialCreationOptions $publicKey, string $data, string $keyName): WebauthnKey
+    {
+        $publicKeyCredentialSource = $this->app->make(PublicKeyCredentialValidator::class)
+            ->validate($publicKey, $data);
+
+        $webauthnKey = $this->create($user, $keyName, $publicKeyCredentialSource);
+
+        $this->forceAuthenticate();
+
+        $this->events->dispatch(new WebauthnRegister($webauthnKey));
+
+        return $webauthnKey;
+    }
+
+    /**
+     * Create a new key.
+     *
+     * @param User $user
+     * @param string $keyName
+     * @param PublicKeyCredentialSource $publicKeyCredentialSource
+     * @return WebauthnKey
+     */
+    public function create(User $user, string $keyName, PublicKeyCredentialSource $publicKeyCredentialSource)
+    {
+        $webauthnKey = WebauthnKey::make([
+            'user_id' => $user->getAuthIdentifier(),
+            'name' => $keyName,
+        ]);
+
+        $webauthnKey->publicKeyCredentialSource = $publicKeyCredentialSource;
+        $webauthnKey->save();
+
+        return $webauthnKey;
+    }
+}

+ 1 - 0
composer.json

@@ -9,6 +9,7 @@
     "license": "MIT",
     "license": "MIT",
     "require": {
     "require": {
         "php": "^7.3",
         "php": "^7.3",
+        "asbiin/laravel-webauthn": "^0.9.0",
         "bacon/bacon-qr-code": "^2.0",
         "bacon/bacon-qr-code": "^2.0",
         "doctrine/dbal": "^2.9",
         "doctrine/dbal": "^2.9",
         "fideloper/proxy": "^4.2",
         "fideloper/proxy": "^4.2",

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 730 - 54
composer.lock


+ 2 - 2
config/version.yml

@@ -5,9 +5,9 @@ current:
   major: 0
   major: 0
   minor: 6
   minor: 6
   patch: 0
   patch: 0
-  prerelease: 1-geaed93a
+  prerelease: 2-g2bfc353
   buildmetadata: ''
   buildmetadata: ''
-  commit: eaed93
+  commit: 2bfc35
   timestamp:
   timestamp:
     year: 2020
     year: 2020
     month: 10
     month: 10

+ 225 - 0
config/webauthn.php

@@ -0,0 +1,225 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | LaravelWebauthn Master Switch
+    |--------------------------------------------------------------------------
+    |
+    | This option may be used to disable LaravelWebauthn.
+    |
+    */
+
+    'enable' => true,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Route Middleware
+    |--------------------------------------------------------------------------
+    |
+    | These middleware will be assigned to Webauthn routes, giving you
+    | the chance to add your own middleware to this list or change any of
+    | the existing middleware. Or, you can simply stick with this list.
+    |
+    */
+
+    'middleware' => [
+        'web',
+        'auth',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Prefix path
+    |--------------------------------------------------------------------------
+    |
+    | The uri prefix for all webauthn requests.
+    |
+    */
+
+    'prefix' => 'webauthn',
+
+    'authenticate' => [
+        /*
+        |--------------------------------------------------------------------------
+        | View to load after middleware login request.
+        |--------------------------------------------------------------------------
+        |
+        | The name of blade template to load whe a user login and it request to validate
+        | the Webauthn 2nd factor.
+        |
+        */
+        'view' => 'webauthn::authenticate',
+
+        /*
+        |--------------------------------------------------------------------------
+        | Redirect with callback url after login.
+        |--------------------------------------------------------------------------
+        |
+        | Save the destination url, then after a succesful login, redirect to this
+        | url.
+        |
+        */
+        'postSuccessCallback' => true,
+
+        /*
+        |--------------------------------------------------------------------------
+        | Redirect route
+        |--------------------------------------------------------------------------
+        |
+        | If postSuccessCallback if false, redirect to this route after login
+        | request is complete.
+        | If empty, send a json response to let the client side redirection.
+        |
+        */
+        'postSuccessRedirectRoute' => '',
+    ],
+
+    'register' => [
+        /*
+        |--------------------------------------------------------------------------
+        | View to load on register request.
+        |--------------------------------------------------------------------------
+        |
+        | The name of blade template to load when a user request a creation of
+        | Webauthn key.
+        |
+        */
+        'view' => 'webauthn::register',
+
+        /*
+        |--------------------------------------------------------------------------
+        | Redirect route
+        |--------------------------------------------------------------------------
+        |
+        | The route to redirect to after register key request is complete.
+        | If empty, send a json response to let the client side redirection.
+        |
+        */
+        'postSuccessRedirectRoute' => '/settings',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session name
+    |--------------------------------------------------------------------------
+    |
+    | Name of the session parameter to store the successful login.
+    |
+    */
+
+    'sessionName' => 'webauthn_auth',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn challenge length
+    |--------------------------------------------------------------------------
+    |
+    | Length of the random string used in the challenge request.
+    |
+    */
+
+    'challenge_length' => 32,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn timeout (milliseconds)
+    |--------------------------------------------------------------------------
+    |
+    | Time that the caller is willing to wait for the call to complete.
+    |
+    */
+
+    'timeout' => 60000,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn extension client input
+    |--------------------------------------------------------------------------
+    |
+    | Optional authentication extension.
+    | See https://www.w3.org/TR/webauthn/#client-extension-input
+    |
+    */
+
+    'extensions' => [],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn icon
+    |--------------------------------------------------------------------------
+    |
+    | Url which resolves to an image associated with the entity.
+    | See https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon
+    |
+    */
+
+    'icon' => null,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn Attestation Conveyance
+    |--------------------------------------------------------------------------
+    |
+    | This parameter specify the preference regarding the attestation conveyance
+    | during credential generation.
+    | See https://www.w3.org/TR/webauthn/#attestation-convey
+    |
+    */
+
+    'attestation_conveyance' => \Webauthn\PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Google Safetynet ApiKey
+    |--------------------------------------------------------------------------
+    |
+    | Api key to use Google Safetynet.
+    | See https://developer.android.com/training/safetynet/attestation
+    |
+    */
+
+    'google_safetynet_api_key' => '',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn Public Key Credential Parameters
+    |--------------------------------------------------------------------------
+    |
+    | List of allowed Cryptographic Algorithm Identifier.
+    | See https://www.w3.org/TR/webauthn/#alg-identifier
+    |
+    */
+
+    'public_key_credential_parameters' => [
+        \Cose\Algorithms::COSE_ALGORITHM_ES256,
+        \Cose\Algorithms::COSE_ALGORITHM_RS256,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Webauthn Authenticator Selection Criteria
+    |--------------------------------------------------------------------------
+    |
+    | Requirement for the creation operation.
+    | See https://www.w3.org/TR/webauthn/#authenticatorSelection
+    |
+    */
+
+    'authenticator_selection_criteria' => [
+
+        /*
+        | See https://www.w3.org/TR/webauthn/#attachment
+        */
+        'attachment_mode' => \Webauthn\AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
+
+        'require_resident_key' => false,
+
+        /*
+        | See https://www.w3.org/TR/webauthn/#userVerificationRequirement
+        */
+        'user_verification' => \Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED,
+    ],
+
+];

+ 0 - 0
database/factories/DeletedUsername.php → database/factories/DeletedUsernameFactory.php


+ 44 - 0
database/migrations/2020_11_24_120152_create_webauthn_keys_table.php

@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateWebauthnKeysTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('webauthn_keys', function (Blueprint $table) {
+            $table->uuid('id');
+            $table->uuid('user_id');
+            $table->string('name')->default('key');
+            $table->string('credentialId', 255)->index();
+            $table->string('type', 255);
+            $table->text('transports');
+            $table->string('attestationType', 255);
+            $table->text('trustPath');
+            $table->text('aaguid');
+            $table->text('credentialPublicKey');
+            $table->integer('counter');
+            $table->timestamps();
+
+            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+            $table->primary('id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('webauthn_keys');
+    }
+}

+ 163 - 204
package-lock.json

@@ -11,23 +11,23 @@
             }
             }
         },
         },
         "@babel/compat-data": {
         "@babel/compat-data": {
-            "version": "7.12.5",
-            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.5.tgz",
-            "integrity": "sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg=="
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz",
+            "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw=="
         },
         },
         "@babel/core": {
         "@babel/core": {
-            "version": "7.12.3",
-            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.3.tgz",
-            "integrity": "sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g==",
+            "version": "7.12.8",
+            "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.8.tgz",
+            "integrity": "sha512-ra28JXL+5z73r1IC/t+FT1ApXU5LsulFDnTDntNfLQaScJUJmcHL5Qxm/IWanCToQk3bPWQo5bflbplU5r15pg==",
             "requires": {
             "requires": {
                 "@babel/code-frame": "^7.10.4",
                 "@babel/code-frame": "^7.10.4",
-                "@babel/generator": "^7.12.1",
+                "@babel/generator": "^7.12.5",
                 "@babel/helper-module-transforms": "^7.12.1",
                 "@babel/helper-module-transforms": "^7.12.1",
-                "@babel/helpers": "^7.12.1",
-                "@babel/parser": "^7.12.3",
-                "@babel/template": "^7.10.4",
-                "@babel/traverse": "^7.12.1",
-                "@babel/types": "^7.12.1",
+                "@babel/helpers": "^7.12.5",
+                "@babel/parser": "^7.12.7",
+                "@babel/template": "^7.12.7",
+                "@babel/traverse": "^7.12.8",
+                "@babel/types": "^7.12.7",
                 "convert-source-map": "^1.7.0",
                 "convert-source-map": "^1.7.0",
                 "debug": "^4.1.0",
                 "debug": "^4.1.0",
                 "gensync": "^1.0.0-beta.1",
                 "gensync": "^1.0.0-beta.1",
@@ -39,9 +39,9 @@
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "4.2.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-                    "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+                    "version": "4.3.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+                    "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
                     "requires": {
                     "requires": {
                         "ms": "2.1.2"
                         "ms": "2.1.2"
                     }
                     }
@@ -104,12 +104,11 @@
             }
             }
         },
         },
         "@babel/helper-create-regexp-features-plugin": {
         "@babel/helper-create-regexp-features-plugin": {
-            "version": "7.12.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz",
-            "integrity": "sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz",
+            "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==",
             "requires": {
             "requires": {
                 "@babel/helper-annotate-as-pure": "^7.10.4",
                 "@babel/helper-annotate-as-pure": "^7.10.4",
-                "@babel/helper-regex": "^7.10.4",
                 "regexpu-core": "^4.7.1"
                 "regexpu-core": "^4.7.1"
             }
             }
         },
         },
@@ -158,11 +157,11 @@
             }
             }
         },
         },
         "@babel/helper-member-expression-to-functions": {
         "@babel/helper-member-expression-to-functions": {
-            "version": "7.12.1",
-            "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz",
-            "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz",
+            "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==",
             "requires": {
             "requires": {
-                "@babel/types": "^7.12.1"
+                "@babel/types": "^7.12.7"
             }
             }
         },
         },
         "@babel/helper-module-imports": {
         "@babel/helper-module-imports": {
@@ -190,11 +189,11 @@
             }
             }
         },
         },
         "@babel/helper-optimise-call-expression": {
         "@babel/helper-optimise-call-expression": {
-            "version": "7.10.4",
-            "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz",
-            "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz",
+            "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==",
             "requires": {
             "requires": {
-                "@babel/types": "^7.10.4"
+                "@babel/types": "^7.12.7"
             }
             }
         },
         },
         "@babel/helper-plugin-utils": {
         "@babel/helper-plugin-utils": {
@@ -202,14 +201,6 @@
             "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
             "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
             "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg=="
             "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg=="
         },
         },
-        "@babel/helper-regex": {
-            "version": "7.10.5",
-            "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz",
-            "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==",
-            "requires": {
-                "lodash": "^4.17.19"
-            }
-        },
         "@babel/helper-remap-async-to-generator": {
         "@babel/helper-remap-async-to-generator": {
             "version": "7.12.1",
             "version": "7.12.1",
             "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz",
             "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz",
@@ -297,9 +288,9 @@
             }
             }
         },
         },
         "@babel/parser": {
         "@babel/parser": {
-            "version": "7.12.5",
-            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz",
-            "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ=="
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz",
+            "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg=="
         },
         },
         "@babel/plugin-proposal-async-generator-functions": {
         "@babel/plugin-proposal-async-generator-functions": {
             "version": "7.12.1",
             "version": "7.12.1",
@@ -366,9 +357,9 @@
             }
             }
         },
         },
         "@babel/plugin-proposal-numeric-separator": {
         "@babel/plugin-proposal-numeric-separator": {
-            "version": "7.12.5",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz",
-            "integrity": "sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz",
+            "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==",
             "requires": {
             "requires": {
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/plugin-syntax-numeric-separator": "^7.10.4"
                 "@babel/plugin-syntax-numeric-separator": "^7.10.4"
@@ -394,9 +385,9 @@
             }
             }
         },
         },
         "@babel/plugin-proposal-optional-chaining": {
         "@babel/plugin-proposal-optional-chaining": {
-            "version": "7.12.1",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz",
-            "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz",
+            "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==",
             "requires": {
             "requires": {
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1",
                 "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1",
@@ -769,12 +760,11 @@
             }
             }
         },
         },
         "@babel/plugin-transform-sticky-regex": {
         "@babel/plugin-transform-sticky-regex": {
-            "version": "7.12.1",
-            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz",
-            "integrity": "sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz",
+            "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==",
             "requires": {
             "requires": {
-                "@babel/helper-plugin-utils": "^7.10.4",
-                "@babel/helper-regex": "^7.10.4"
+                "@babel/helper-plugin-utils": "^7.10.4"
             }
             }
         },
         },
         "@babel/plugin-transform-template-literals": {
         "@babel/plugin-transform-template-literals": {
@@ -811,13 +801,13 @@
             }
             }
         },
         },
         "@babel/preset-env": {
         "@babel/preset-env": {
-            "version": "7.12.1",
-            "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz",
-            "integrity": "sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz",
+            "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==",
             "requires": {
             "requires": {
-                "@babel/compat-data": "^7.12.1",
-                "@babel/helper-compilation-targets": "^7.12.1",
-                "@babel/helper-module-imports": "^7.12.1",
+                "@babel/compat-data": "^7.12.7",
+                "@babel/helper-compilation-targets": "^7.12.5",
+                "@babel/helper-module-imports": "^7.12.5",
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/helper-plugin-utils": "^7.10.4",
                 "@babel/helper-validator-option": "^7.12.1",
                 "@babel/helper-validator-option": "^7.12.1",
                 "@babel/plugin-proposal-async-generator-functions": "^7.12.1",
                 "@babel/plugin-proposal-async-generator-functions": "^7.12.1",
@@ -827,10 +817,10 @@
                 "@babel/plugin-proposal-json-strings": "^7.12.1",
                 "@babel/plugin-proposal-json-strings": "^7.12.1",
                 "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1",
                 "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1",
                 "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
                 "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
-                "@babel/plugin-proposal-numeric-separator": "^7.12.1",
+                "@babel/plugin-proposal-numeric-separator": "^7.12.7",
                 "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
                 "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
                 "@babel/plugin-proposal-optional-catch-binding": "^7.12.1",
                 "@babel/plugin-proposal-optional-catch-binding": "^7.12.1",
-                "@babel/plugin-proposal-optional-chaining": "^7.12.1",
+                "@babel/plugin-proposal-optional-chaining": "^7.12.7",
                 "@babel/plugin-proposal-private-methods": "^7.12.1",
                 "@babel/plugin-proposal-private-methods": "^7.12.1",
                 "@babel/plugin-proposal-unicode-property-regex": "^7.12.1",
                 "@babel/plugin-proposal-unicode-property-regex": "^7.12.1",
                 "@babel/plugin-syntax-async-generators": "^7.8.0",
                 "@babel/plugin-syntax-async-generators": "^7.8.0",
@@ -872,14 +862,14 @@
                 "@babel/plugin-transform-reserved-words": "^7.12.1",
                 "@babel/plugin-transform-reserved-words": "^7.12.1",
                 "@babel/plugin-transform-shorthand-properties": "^7.12.1",
                 "@babel/plugin-transform-shorthand-properties": "^7.12.1",
                 "@babel/plugin-transform-spread": "^7.12.1",
                 "@babel/plugin-transform-spread": "^7.12.1",
-                "@babel/plugin-transform-sticky-regex": "^7.12.1",
+                "@babel/plugin-transform-sticky-regex": "^7.12.7",
                 "@babel/plugin-transform-template-literals": "^7.12.1",
                 "@babel/plugin-transform-template-literals": "^7.12.1",
                 "@babel/plugin-transform-typeof-symbol": "^7.12.1",
                 "@babel/plugin-transform-typeof-symbol": "^7.12.1",
                 "@babel/plugin-transform-unicode-escapes": "^7.12.1",
                 "@babel/plugin-transform-unicode-escapes": "^7.12.1",
                 "@babel/plugin-transform-unicode-regex": "^7.12.1",
                 "@babel/plugin-transform-unicode-regex": "^7.12.1",
                 "@babel/preset-modules": "^0.1.3",
                 "@babel/preset-modules": "^0.1.3",
-                "@babel/types": "^7.12.1",
-                "core-js-compat": "^3.6.2",
+                "@babel/types": "^7.12.7",
+                "core-js-compat": "^3.7.0",
                 "semver": "^5.5.0"
                 "semver": "^5.5.0"
             }
             }
         },
         },
@@ -904,35 +894,35 @@
             }
             }
         },
         },
         "@babel/template": {
         "@babel/template": {
-            "version": "7.10.4",
-            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz",
-            "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz",
+            "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==",
             "requires": {
             "requires": {
                 "@babel/code-frame": "^7.10.4",
                 "@babel/code-frame": "^7.10.4",
-                "@babel/parser": "^7.10.4",
-                "@babel/types": "^7.10.4"
+                "@babel/parser": "^7.12.7",
+                "@babel/types": "^7.12.7"
             }
             }
         },
         },
         "@babel/traverse": {
         "@babel/traverse": {
-            "version": "7.12.5",
-            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz",
-            "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==",
+            "version": "7.12.8",
+            "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.8.tgz",
+            "integrity": "sha512-EIRQXPTwFEGRZyu6gXbjfpNORN1oZvwuzJbxcXjAgWV0iqXYDszN1Hx3FVm6YgZfu1ZQbCVAk3l+nIw95Xll9Q==",
             "requires": {
             "requires": {
                 "@babel/code-frame": "^7.10.4",
                 "@babel/code-frame": "^7.10.4",
                 "@babel/generator": "^7.12.5",
                 "@babel/generator": "^7.12.5",
                 "@babel/helper-function-name": "^7.10.4",
                 "@babel/helper-function-name": "^7.10.4",
                 "@babel/helper-split-export-declaration": "^7.11.0",
                 "@babel/helper-split-export-declaration": "^7.11.0",
-                "@babel/parser": "^7.12.5",
-                "@babel/types": "^7.12.5",
+                "@babel/parser": "^7.12.7",
+                "@babel/types": "^7.12.7",
                 "debug": "^4.1.0",
                 "debug": "^4.1.0",
                 "globals": "^11.1.0",
                 "globals": "^11.1.0",
                 "lodash": "^4.17.19"
                 "lodash": "^4.17.19"
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "4.2.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-                    "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+                    "version": "4.3.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+                    "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
                     "requires": {
                     "requires": {
                         "ms": "2.1.2"
                         "ms": "2.1.2"
                     }
                     }
@@ -945,9 +935,9 @@
             }
             }
         },
         },
         "@babel/types": {
         "@babel/types": {
-            "version": "7.12.6",
-            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz",
-            "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==",
+            "version": "7.12.7",
+            "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz",
+            "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==",
             "requires": {
             "requires": {
                 "@babel/helper-validator-identifier": "^7.10.4",
                 "@babel/helper-validator-identifier": "^7.10.4",
                 "lodash": "^4.17.19",
                 "lodash": "^4.17.19",
@@ -1031,9 +1021,9 @@
             "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
             "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
         },
         },
         "@types/node": {
         "@types/node": {
-            "version": "14.14.6",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
-            "integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
+            "version": "14.14.9",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
+            "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw=="
         },
         },
         "@types/normalize-package-data": {
         "@types/normalize-package-data": {
             "version": "2.4.0",
             "version": "2.4.0",
@@ -1600,13 +1590,13 @@
             }
             }
         },
         },
         "babel-loader": {
         "babel-loader": {
-            "version": "8.1.0",
-            "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz",
-            "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==",
+            "version": "8.2.1",
+            "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.1.tgz",
+            "integrity": "sha512-dMF8sb2KQ8kJl21GUjkW1HWmcsL39GOV5vnzjqrCzEPNY0S0UfMLnumidiwIajDSBmKhYf5iRW+HXaM4cvCKBw==",
             "requires": {
             "requires": {
                 "find-cache-dir": "^2.1.0",
                 "find-cache-dir": "^2.1.0",
                 "loader-utils": "^1.4.0",
                 "loader-utils": "^1.4.0",
-                "mkdirp": "^0.5.3",
+                "make-dir": "^2.1.0",
                 "pify": "^4.0.1",
                 "pify": "^4.0.1",
                 "schema-utils": "^2.6.5"
                 "schema-utils": "^2.6.5"
             }
             }
@@ -1685,9 +1675,9 @@
             }
             }
         },
         },
         "base64-js": {
         "base64-js": {
-            "version": "1.3.1",
-            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
-            "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+            "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
         },
         },
         "batch": {
         "batch": {
             "version": "0.6.1",
             "version": "0.6.1",
@@ -1854,19 +1844,12 @@
             }
             }
         },
         },
         "browserify-rsa": {
         "browserify-rsa": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
-            "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
+            "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
             "requires": {
             "requires": {
-                "bn.js": "^4.1.0",
+                "bn.js": "^5.0.0",
                 "randombytes": "^2.0.1"
                 "randombytes": "^2.0.1"
-            },
-            "dependencies": {
-                "bn.js": {
-                    "version": "4.11.9",
-                    "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
-                    "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
-                }
             }
             }
         },
         },
         "browserify-sign": {
         "browserify-sign": {
@@ -2263,9 +2246,9 @@
             "dev": true
             "dev": true
         },
         },
         "collect.js": {
         "collect.js": {
-            "version": "4.28.4",
-            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.28.4.tgz",
-            "integrity": "sha512-NJXATt6r+gtGOgDJOKLeooTY6QpGn8YQN/PkKnCmajJOguz/xGPgPrTyrBkmBBTHXnniPRIkUqjqt3AkjwCKlg=="
+            "version": "4.28.6",
+            "resolved": "https://registry.npmjs.org/collect.js/-/collect.js-4.28.6.tgz",
+            "integrity": "sha512-NAyuk1DnCotRaDZIS5kJ4sptgkwOeYqElird10yziN5JBuwYOGkOTguhNcPn5g344IfylZecxNYZAVXgv19p5Q=="
         },
         },
         "collection-visit": {
         "collection-visit": {
             "version": "1.0.0",
             "version": "1.0.0",
@@ -2464,11 +2447,11 @@
             "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
             "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
         },
         },
         "core-js-compat": {
         "core-js-compat": {
-            "version": "3.6.5",
-            "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
-            "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==",
+            "version": "3.7.0",
+            "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.7.0.tgz",
+            "integrity": "sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg==",
             "requires": {
             "requires": {
-                "browserslist": "^4.8.5",
+                "browserslist": "^4.14.6",
                 "semver": "7.0.0"
                 "semver": "7.0.0"
             },
             },
             "dependencies": {
             "dependencies": {
@@ -2778,26 +2761,26 @@
             "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q=="
             "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q=="
         },
         },
         "csso": {
         "csso": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/csso/-/csso-4.1.0.tgz",
-            "integrity": "sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg==",
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/csso/-/csso-4.1.1.tgz",
+            "integrity": "sha512-Rvq+e1e0TFB8E8X+8MQjHSY6vtol45s5gxtLI/018UsAn2IBMmwNEZRM/h+HVnAJRHjasLIKKUO3uvoMM28LvA==",
             "requires": {
             "requires": {
                 "css-tree": "^1.0.0"
                 "css-tree": "^1.0.0"
             },
             },
             "dependencies": {
             "dependencies": {
                 "css-tree": {
                 "css-tree": {
-                    "version": "1.0.0",
-                    "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0.tgz",
-                    "integrity": "sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw==",
+                    "version": "1.1.1",
+                    "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.1.tgz",
+                    "integrity": "sha512-NVN42M2fjszcUNpDbdkvutgQSlFYsr1z7kqeuCagHnNLBfYor6uP1WL1KrkmdYZ5Y1vTBCIOI/C/+8T98fJ71w==",
                     "requires": {
                     "requires": {
-                        "mdn-data": "2.0.12",
+                        "mdn-data": "2.0.14",
                         "source-map": "^0.6.1"
                         "source-map": "^0.6.1"
                     }
                     }
                 },
                 },
                 "mdn-data": {
                 "mdn-data": {
-                    "version": "2.0.12",
-                    "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz",
-                    "integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q=="
+                    "version": "2.0.14",
+                    "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+                    "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
                 },
                 },
                 "source-map": {
                 "source-map": {
                     "version": "0.6.1",
                     "version": "0.6.1",
@@ -2826,9 +2809,9 @@
             "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ=="
             "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ=="
         },
         },
         "dayjs": {
         "dayjs": {
-            "version": "1.9.5",
-            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.5.tgz",
-            "integrity": "sha512-WULIw7UpW/E0y6VywewpbXAMH3d5cZijEhoHLwM+OMVbk/NtchKS/W+57H/0P1rqU7gHrAArjiRLHCUhgMQl6w=="
+            "version": "1.9.6",
+            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.6.tgz",
+            "integrity": "sha512-HngNLtPEBWRo8EFVmHFmSXAjtCX8rGNqeXQI0Gh7wCTSqwaKgPIDqu9m07wABVopNwzvOeCb+2711vQhDlcIXw=="
         },
         },
         "de-indent": {
         "de-indent": {
             "version": "1.0.2",
             "version": "1.0.2",
@@ -4786,6 +4769,11 @@
             "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
             "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
             "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
             "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
         },
         },
+        "is-docker": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
+            "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw=="
+        },
         "is-extendable": {
         "is-extendable": {
             "version": "1.0.1",
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
             "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
@@ -5047,9 +5035,9 @@
             "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
             "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
         },
         },
         "laravel-mix": {
         "laravel-mix": {
-            "version": "5.0.7",
-            "resolved": "https://registry.npmjs.org/laravel-mix/-/laravel-mix-5.0.7.tgz",
-            "integrity": "sha512-TL5txnQkzcwM8DYckgzjISSPGyZN6znFYb4NgtTSi9aIvfzOIEC6p0eYM6wDa/BkEKv290Ru6HWmH6Q2XApogQ==",
+            "version": "5.0.9",
+            "resolved": "https://registry.npmjs.org/laravel-mix/-/laravel-mix-5.0.9.tgz",
+            "integrity": "sha512-1WCJiHimTRW3KlxcabRTco0q+bo4uKPaFTkc6cJ/bLEq4JT1aPkojoauUK7+PyiIlDJncw0Nt2MtDrv5C6j5IQ==",
             "requires": {
             "requires": {
                 "@babel/core": "^7.2.0",
                 "@babel/core": "^7.2.0",
                 "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
                 "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
@@ -5869,17 +5857,30 @@
             }
             }
         },
         },
         "node-notifier": {
         "node-notifier": {
-            "version": "5.4.3",
-            "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz",
-            "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==",
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz",
+            "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==",
             "requires": {
             "requires": {
                 "growly": "^1.3.0",
                 "growly": "^1.3.0",
-                "is-wsl": "^1.1.0",
-                "semver": "^5.5.0",
+                "is-wsl": "^2.1.1",
+                "semver": "^6.3.0",
                 "shellwords": "^0.1.1",
                 "shellwords": "^0.1.1",
-                "which": "^1.3.0"
+                "which": "^1.3.1"
             },
             },
             "dependencies": {
             "dependencies": {
+                "is-wsl": {
+                    "version": "2.2.0",
+                    "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+                    "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+                    "requires": {
+                        "is-docker": "^2.0.0"
+                    }
+                },
+                "semver": {
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+                    "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+                },
                 "which": {
                 "which": {
                     "version": "1.3.1",
                     "version": "1.3.1",
                     "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
                     "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -6436,9 +6437,9 @@
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "3.2.6",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-                    "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+                    "version": "3.2.7",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+                    "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
                     "requires": {
                     "requires": {
                         "ms": "^2.1.1"
                         "ms": "^2.1.1"
                     }
                     }
@@ -8214,9 +8215,9 @@
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "3.2.6",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-                    "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+                    "version": "3.2.7",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+                    "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
                     "requires": {
                     "requires": {
                         "ms": "^2.1.1"
                         "ms": "^2.1.1"
                     }
                     }
@@ -8329,9 +8330,9 @@
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "4.2.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-                    "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+                    "version": "4.3.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+                    "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
                     "requires": {
                     "requires": {
                         "ms": "2.1.2"
                         "ms": "2.1.2"
                     }
                     }
@@ -8357,9 +8358,9 @@
             },
             },
             "dependencies": {
             "dependencies": {
                 "debug": {
                 "debug": {
-                    "version": "4.2.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-                    "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+                    "version": "4.3.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+                    "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
                     "requires": {
                     "requires": {
                         "ms": "2.1.2"
                         "ms": "2.1.2"
                     }
                     }
@@ -8509,63 +8510,21 @@
             }
             }
         },
         },
         "string.prototype.trimend": {
         "string.prototype.trimend": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz",
-            "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==",
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
+            "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
             "requires": {
             "requires": {
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.18.0-next.1"
-            },
-            "dependencies": {
-                "es-abstract": {
-                    "version": "1.18.0-next.1",
-                    "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
-                    "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
-                    "requires": {
-                        "es-to-primitive": "^1.2.1",
-                        "function-bind": "^1.1.1",
-                        "has": "^1.0.3",
-                        "has-symbols": "^1.0.1",
-                        "is-callable": "^1.2.2",
-                        "is-negative-zero": "^2.0.0",
-                        "is-regex": "^1.1.1",
-                        "object-inspect": "^1.8.0",
-                        "object-keys": "^1.1.1",
-                        "object.assign": "^4.1.1",
-                        "string.prototype.trimend": "^1.0.1",
-                        "string.prototype.trimstart": "^1.0.1"
-                    }
-                }
+                "call-bind": "^1.0.0",
+                "define-properties": "^1.1.3"
             }
             }
         },
         },
         "string.prototype.trimstart": {
         "string.prototype.trimstart": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz",
-            "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==",
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
+            "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
             "requires": {
             "requires": {
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.18.0-next.1"
-            },
-            "dependencies": {
-                "es-abstract": {
-                    "version": "1.18.0-next.1",
-                    "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
-                    "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
-                    "requires": {
-                        "es-to-primitive": "^1.2.1",
-                        "function-bind": "^1.1.1",
-                        "has": "^1.0.3",
-                        "has-symbols": "^1.0.1",
-                        "is-callable": "^1.2.2",
-                        "is-negative-zero": "^2.0.0",
-                        "is-regex": "^1.1.1",
-                        "object-inspect": "^1.8.0",
-                        "object-keys": "^1.1.1",
-                        "object.assign": "^4.1.1",
-                        "string.prototype.trimend": "^1.0.1",
-                        "string.prototype.trimstart": "^1.0.1"
-                    }
-                }
+                "call-bind": "^1.0.0",
+                "define-properties": "^1.1.3"
             }
             }
         },
         },
         "string_decoder": {
         "string_decoder": {
@@ -9372,14 +9331,14 @@
             }
             }
         },
         },
         "watchpack": {
         "watchpack": {
-            "version": "1.7.4",
-            "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz",
-            "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==",
+            "version": "1.7.5",
+            "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz",
+            "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==",
             "requires": {
             "requires": {
                 "chokidar": "^3.4.1",
                 "chokidar": "^3.4.1",
                 "graceful-fs": "^4.1.2",
                 "graceful-fs": "^4.1.2",
                 "neo-async": "^2.5.0",
                 "neo-async": "^2.5.0",
-                "watchpack-chokidar2": "^2.0.0"
+                "watchpack-chokidar2": "^2.0.1"
             },
             },
             "dependencies": {
             "dependencies": {
                 "anymatch": {
                 "anymatch": {
@@ -9483,9 +9442,9 @@
             }
             }
         },
         },
         "watchpack-chokidar2": {
         "watchpack-chokidar2": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz",
-            "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==",
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz",
+            "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==",
             "optional": true,
             "optional": true,
             "requires": {
             "requires": {
                 "chokidar": "^2.1.8"
                 "chokidar": "^2.1.8"
@@ -9773,9 +9732,9 @@
                     "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
                     "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
                 },
                 },
                 "debug": {
                 "debug": {
-                    "version": "4.2.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-                    "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+                    "version": "4.3.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+                    "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
                     "requires": {
                     "requires": {
                         "ms": "2.1.2"
                         "ms": "2.1.2"
                     }
                     }
@@ -9870,11 +9829,11 @@
             }
             }
         },
         },
         "webpack-notifier": {
         "webpack-notifier": {
-            "version": "1.8.0",
-            "resolved": "https://registry.npmjs.org/webpack-notifier/-/webpack-notifier-1.8.0.tgz",
-            "integrity": "sha512-I6t76NoPe5DZCCm5geELmDV2wlJ89LbU425uN6T2FG8Ywrrt1ZcUMz6g8yWGNg4pttqTPFQJYUPjWAlzUEQ+cQ==",
+            "version": "1.10.1",
+            "resolved": "https://registry.npmjs.org/webpack-notifier/-/webpack-notifier-1.10.1.tgz",
+            "integrity": "sha512-1ntvm2QwT+i21Nur88HU/WaaDq1Ppq1XCpBR0//wb6gPJ65IkRkuYzIu8z8MpnkLEHj9wSf+yNUzMANyqvvtWg==",
             "requires": {
             "requires": {
-                "node-notifier": "^5.1.2",
+                "node-notifier": "^6.0.0",
                 "object-assign": "^4.1.0",
                 "object-assign": "^4.1.0",
                 "strip-ansi": "^3.0.1"
                 "strip-ansi": "^3.0.1"
             }
             }

+ 2 - 2
package.json

@@ -13,8 +13,8 @@
     "dependencies": {
     "dependencies": {
         "axios": "^0.19",
         "axios": "^0.19",
         "cross-env": "^7.0",
         "cross-env": "^7.0",
-        "dayjs": "^1.9.3",
-        "laravel-mix": "^5.0.7",
+        "dayjs": "^1.9.6",
+        "laravel-mix": "^5.0.9",
         "lodash": "^4.17.20",
         "lodash": "^4.17.20",
         "portal-vue": "^2.1.7",
         "portal-vue": "^2.1.7",
         "postcss-import": "^11.1.0",
         "postcss-import": "^11.1.0",

+ 1 - 0
resources/js/app.js

@@ -35,6 +35,7 @@ Vue.component(
   'passport-personal-access-tokens',
   'passport-personal-access-tokens',
   require('./components/passport/PersonalAccessTokens.vue').default
   require('./components/passport/PersonalAccessTokens.vue').default
 )
 )
+Vue.component('webauthn-keys', require('./components/WebauthnKeys.vue').default)
 
 
 Vue.filter('formatDate', value => {
 Vue.filter('formatDate', value => {
   return dayjs(value).format('Do MMM YYYY')
   return dayjs(value).format('Do MMM YYYY')

+ 127 - 0
resources/js/components/WebauthnKeys.vue

@@ -0,0 +1,127 @@
+<template>
+  <div>
+    <div class="mt-6">
+      <h3 class="font-bold text-xl">
+        Device Authentication (U2F)
+      </h3>
+
+      <div class="my-4 w-24 border-b-2 border-grey-200"></div>
+
+      <p class="my-6">
+        Webauthn Keys you have registered for 2nd factor authentication. To remove a key simply
+        click the delete button next to it.
+      </p>
+
+      <div>
+        <p class="mb-0" v-if="keys.length === 0">
+          You have not registered any Webauthn Keys.
+        </p>
+
+        <div class="table w-full text-sm md:text-base" v-if="keys.length > 0">
+          <div class="table-row">
+            <div class="table-cell p-1 md:p-4 font-semibold">Name</div>
+            <div class="table-cell p-1 md:p-4 font-semibold">Created</div>
+            <div class="table-cell p-1 md:p-4 text-right">
+              <a href="/webauthn/register" class="text-indigo-700">Add New Device</a>
+            </div>
+          </div>
+          <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.created_at | timeAgo }}</div>
+            <div class="table-cell p-1 md:p-4 text-right">
+              <a
+                class="text-red-500 font-bold cursor-pointer focus:outline-none"
+                @click="showRemoveModal(key)"
+              >
+                Delete
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <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 U2F Device
+        </h2>
+        <p class="my-4 text-grey-700">
+          Once this device is removed, <b>Two-Factor Authentication</b> will be disabled on your
+          account.
+        </p>
+        <div class="mt-6">
+          <button
+            @click="remove"
+            class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus:outline-none"
+            :class="removeKeyLoading ? 'cursor-not-allowed' : ''"
+            :disabled="removeKeyLoading"
+          >
+            Remove
+            <loader v-if="removeKeyLoading" />
+          </button>
+          <button
+            @click="closeDeleteKeyModal"
+            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>
+    </Modal>
+  </div>
+</template>
+
+<script>
+import Modal from './Modal.vue'
+
+export default {
+  components: {
+    Modal,
+  },
+  data() {
+    return {
+      deleteKeyModalOpen: false,
+      keys: [],
+      keyToRemove: null,
+      loading: false,
+      removeKeyLoading: false,
+    }
+  },
+  mounted() {
+    this.getWebauthnKeys()
+  },
+
+  methods: {
+    getWebauthnKeys() {
+      axios.get('/webauthn/keys').then(response => {
+        this.keys = response.data
+      })
+    },
+    showRemoveModal(token) {
+      this.keyToRemove = token
+      this.deleteKeyModalOpen = true
+    },
+    remove() {
+      this.removeKeyLoading = true
+
+      axios.delete(`/webauthn/${this.keyToRemove.id}`).then(response => {
+        this.removeKeyLoading = false
+        this.deleteKeyModalOpen = false
+        this.keyToRemove = null
+
+        if (this.keys.length === 1) {
+          location.reload()
+        } else {
+          this.getWebauthnKeys()
+        }
+      })
+    },
+    closeDeleteKeyModal() {
+      this.deleteKeyModalOpen = false
+    },
+  },
+}
+</script>

+ 12 - 12
resources/js/pages/Aliases.vue

@@ -11,7 +11,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ totalActive }}
             {{ totalActive }}
-            <p class="text-grey-200 text-sm tracking-wide uppercase">
+            <p class="text-grey-300 text-sm tracking-wide uppercase">
               Active
               Active
             </p>
             </p>
           </div>
           </div>
@@ -27,7 +27,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ totalInactive }}
             {{ totalInactive }}
-            <p class="text-grey-200 text-sm tracking-wide uppercase">
+            <p class="text-grey-300 text-sm tracking-wide uppercase">
               Inactive
               Inactive
             </p>
             </p>
           </div>
           </div>
@@ -43,7 +43,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ totalForwarded }}
             {{ totalForwarded }}
-            <p class="text-grey-200 text-sm tracking-wide uppercase">
+            <p class="text-grey-300 text-sm tracking-wide uppercase">
               Emails Forwarded
               Emails Forwarded
             </p>
             </p>
           </div>
           </div>
@@ -59,7 +59,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ totalBlocked }}
             {{ totalBlocked }}
-            <p class="text-grey-200 text-sm tracking-wide uppercase">
+            <p class="text-grey-300 text-sm tracking-wide uppercase">
               Emails Blocked
               Emails Blocked
             </p>
             </p>
           </div>
           </div>
@@ -75,7 +75,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ totalReplies }}
             {{ totalReplies }}
-            <p class="text-grey-200 text-sm tracking-wide uppercase">
+            <p class="text-grey-300 text-sm tracking-wide uppercase">
               Email Replies
               Email Replies
             </p>
             </p>
           </div>
           </div>
@@ -91,7 +91,7 @@
           />
           />
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
           <div class="font-bold text-xl md:text-3xl text-indigo-800">
             {{ bandwidthMb }}<span class="text-sm tracking-wide uppercase">MB</span>
             {{ bandwidthMb }}<span class="text-sm tracking-wide uppercase">MB</span>
-            <p class="text-grey-200 text-sm tracking-wide uppercase">Bandwidth ({{ month }})</p>
+            <p class="text-grey-300 text-sm tracking-wide uppercase">Bandwidth ({{ month }})</p>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -231,7 +231,7 @@
             </span>
             </span>
             <icon
             <icon
               name="edit"
               name="edit"
-              class="inline-block w-6 h-6 ml-2 text-grey-200 fill-current cursor-pointer"
+              class="inline-block w-6 h-6 ml-2 text-grey-300 fill-current cursor-pointer"
               @click.native="
               @click.native="
                 ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
                 ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
               "
               "
@@ -239,7 +239,7 @@
           </div>
           </div>
           <div v-else>
           <div v-else>
             <span
             <span
-              class="inline-block text-grey-200 text-sm cursor-pointer py-1 border border-transparent"
+              class="inline-block text-grey-300 text-sm cursor-pointer py-1 border border-transparent"
               @click=";(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = '')"
               @click=";(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = '')"
               >Add description</span
               >Add description</span
             >
             >
@@ -275,7 +275,7 @@
           >
           >
           <icon
           <icon
             name="edit"
             name="edit"
-            class="ml-2 inline-block w-6 h-6 text-grey-200 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.native="openAliasRecipientsModal(props.row)"
           />
           />
         </span>
         </span>
@@ -295,7 +295,7 @@
           v-else-if="props.column.field == 'emails_replied'"
           v-else-if="props.column.field == 'emails_replied'"
           class="font-semibold text-indigo-800"
           class="font-semibold text-indigo-800"
         >
         >
-          {{ props.row.emails_replied }} <span class="text-grey-200">/</span>
+          {{ props.row.emails_replied }} <span class="text-grey-300">/</span>
           {{ props.row.emails_sent }}
           {{ props.row.emails_sent }}
         </span>
         </span>
         <span v-else-if="props.column.field === 'active'" class="flex items-center">
         <span v-else-if="props.column.field === 'active'" class="flex items-center">
@@ -309,13 +309,13 @@
           <icon
           <icon
             v-if="props.row.deleted_at"
             v-if="props.row.deleted_at"
             name="undo"
             name="undo"
-            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
+            class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
             @click.native="openRestoreModal(props.row.id)"
             @click.native="openRestoreModal(props.row.id)"
           />
           />
           <icon
           <icon
             v-else
             v-else
             name="trash"
             name="trash"
-            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
+            class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
             @click.native="openDeleteModal(props.row.id)"
             @click.native="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>

+ 5 - 5
resources/js/pages/Domains.vue

@@ -102,7 +102,7 @@
             >
             >
             <icon
             <icon
               name="edit"
               name="edit"
-              class="inline-block w-6 h-6 text-grey-200 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.native="
                 ;(domainIdToEdit = props.row.id), (domainDescriptionToEdit = props.row.description)
                 ;(domainIdToEdit = props.row.id), (domainDescriptionToEdit = props.row.description)
               "
               "
@@ -111,7 +111,7 @@
           <div v-else class="flex justify-center">
           <div v-else class="flex justify-center">
             <icon
             <icon
               name="plus"
               name="plus"
-              class="block w-6 h-6 text-grey-200 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.native=";(domainIdToEdit = props.row.id), (domainDescriptionToEdit = '')"
             />
             />
           </div>
           </div>
@@ -121,14 +121,14 @@
             {{ props.row.default_recipient.email | truncate(30) }}
             {{ props.row.default_recipient.email | truncate(30) }}
             <icon
             <icon
               name="edit"
               name="edit"
-              class="ml-2 inline-block w-6 h-6 text-grey-200 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.native="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-200 fill-current cursor-pointer"
+              class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               @click.native="openDomainDefaultRecipientModal(props.row)"
               @click.native="openDomainDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
@@ -169,7 +169,7 @@
         <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-200 fill-current cursor-pointer"
+            class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             @click.native="openDeleteModal(props.row.id)"
             @click.native="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>

+ 4 - 4
resources/js/pages/Recipients.vue

@@ -63,7 +63,7 @@
               }.anonaddy.com. Separating each key by a full stop.`
               }.anonaddy.com. Separating each key by a full stop.`
             "
             "
           >
           >
-            <icon name="info" class="inline-block w-4 h-4 text-grey-200 fill-current" />
+            <icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
           </span>
           </span>
         </span>
         </span>
         <span v-else>
         <span v-else>
@@ -125,7 +125,7 @@
             />
             />
             <icon
             <icon
               name="fingerprint"
               name="fingerprint"
-              class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-200 fill-current mx-2"
+              class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current mx-2"
               :data-tippy-content="props.row.fingerprint"
               :data-tippy-content="props.row.fingerprint"
               v-clipboard="() => props.row.fingerprint"
               v-clipboard="() => props.row.fingerprint"
               v-clipboard:success="clipboardSuccess"
               v-clipboard:success="clipboardSuccess"
@@ -133,7 +133,7 @@
             />
             />
             <icon
             <icon
               name="delete"
               name="delete"
-              class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-200 fill-current"
+              class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current"
               @click.native="openDeleteRecipientKeyModal(props.row)"
               @click.native="openDeleteRecipientKeyModal(props.row)"
               data-tippy-content="Remove public key"
               data-tippy-content="Remove public key"
             />
             />
@@ -168,7 +168,7 @@
           <icon
           <icon
             v-if="!isDefault(props.row.id)"
             v-if="!isDefault(props.row.id)"
             name="trash"
             name="trash"
-            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
+            class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             @click.native="openDeleteModal(props.row)"
             @click.native="openDeleteModal(props.row)"
           />
           />
         </span>
         </span>

+ 8 - 8
resources/js/pages/Rules.vue

@@ -2,7 +2,7 @@
   <div>
   <div>
     <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
     <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
       <div class="flex items-center">
       <div class="flex items-center">
-        <icon name="move" class="block w-6 h-6 mr-2 text-grey-200 fill-current" />
+        <icon name="move" class="block w-6 h-6 mr-2 text-grey-300 fill-current" />
         You can drag and drop rules to order them.
         You can drag and drop rules to order them.
       </div>
       </div>
       <button
       <button
@@ -30,7 +30,7 @@
             <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-200 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">{{ row.name }} </span>
@@ -47,12 +47,12 @@
             <div class="w-1/5 flex justify-end">
             <div class="w-1/5 flex justify-end">
               <icon
               <icon
                 name="edit"
                 name="edit"
-                class="block w-6 h-6 mr-3 text-grey-200 fill-current cursor-pointer"
+                class="block w-6 h-6 mr-3 text-grey-300 fill-current cursor-pointer"
                 @click.native="openEditModal(row)"
                 @click.native="openEditModal(row)"
               />
               />
               <icon
               <icon
                 name="trash"
                 name="trash"
-                class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
+                class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
                 @click.native="openDeleteModal(row.id)"
                 @click.native="openDeleteModal(row.id)"
               />
               />
             </div>
             </div>
@@ -227,7 +227,7 @@
                   <icon
                   <icon
                     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-200 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.native="deleteCondition(createRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
@@ -371,7 +371,7 @@
                   <icon
                   <icon
                     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-200 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.native="deleteAction(createRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
@@ -562,7 +562,7 @@
                   <icon
                   <icon
                     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-200 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.native="deleteCondition(editRuleObject, key)"
                   />
                   />
                 </div>
                 </div>
@@ -700,7 +700,7 @@
                   <icon
                   <icon
                     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-200 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.native="deleteAction(editRuleObject, key)"
                   />
                   />
                 </div>
                 </div>

+ 5 - 5
resources/js/pages/Usernames.vue

@@ -100,7 +100,7 @@
             }}</span>
             }}</span>
             <icon
             <icon
               name="edit"
               name="edit"
-              class="inline-block w-6 h-6 text-grey-200 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.native="
                 ;(usernameIdToEdit = props.row.id),
                 ;(usernameIdToEdit = props.row.id),
                   (usernameDescriptionToEdit = props.row.description)
                   (usernameDescriptionToEdit = props.row.description)
@@ -110,7 +110,7 @@
           <div v-else class="flex justify-center">
           <div v-else class="flex justify-center">
             <icon
             <icon
               name="plus"
               name="plus"
-              class="block w-6 h-6 text-grey-200 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.native=";(usernameIdToEdit = props.row.id), (usernameDescriptionToEdit = '')"
             />
             />
           </div>
           </div>
@@ -120,14 +120,14 @@
             {{ props.row.default_recipient.email | truncate(30) }}
             {{ props.row.default_recipient.email | truncate(30) }}
             <icon
             <icon
               name="edit"
               name="edit"
-              class="ml-2 inline-block w-6 h-6 text-grey-200 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.native="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-200 fill-current cursor-pointer"
+              class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
               @click.native="openUsernameDefaultRecipientModal(props.row)"
               @click.native="openUsernameDefaultRecipientModal(props.row)"
             />
             />
           </div>
           </div>
@@ -152,7 +152,7 @@
         <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-200 fill-current cursor-pointer"
+            class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
             @click.native="openDeleteModal(props.row.id)"
             @click.native="openDeleteModal(props.row.id)"
           />
           />
         </span>
         </span>

+ 239 - 0
resources/js/webauthn.js

@@ -0,0 +1,239 @@
+/**
+ * WebAuthn client.
+ *
+ * This file is part of asbiin/laravel-webauthn project.
+ *
+ * @copyright Alexis SAETTLER © 2019
+ * @license MIT
+ */
+
+'use strict'
+
+/**
+ * Create a new instance of WebAuthn.
+ *
+ * @param {function(string, bool)} notifyCallback
+ * @constructor
+ */
+function WebAuthn(notifyCallback = null) {
+  if (notifyCallback) {
+    this.setNotify(notifyCallback)
+  }
+}
+
+/**
+ * Register a new key.
+ *
+ * @param {PublicKeyCredentialCreationOptions} publicKey  - see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialcreationoptions
+ * @param {function(PublicKeyCredential)} callback  User callback
+ */
+WebAuthn.prototype.register = function(publicKey, callback) {
+  let publicKeyCredential = Object.assign({}, publicKey)
+  publicKeyCredential.user.id = this._bufferDecode(publicKey.user.id)
+  publicKeyCredential.challenge = this._bufferDecode(this._base64Decode(publicKey.challenge))
+  if (publicKey.excludeCredentials) {
+    publicKeyCredential.excludeCredentials = this._credentialDecode(publicKey.excludeCredentials)
+  }
+
+  var self = this
+  navigator.credentials
+    .create({
+      publicKey: publicKeyCredential,
+    })
+    .then(
+      data => {
+        self._registerCallback(data, callback)
+      },
+      error => {
+        self._notify(error.name, error.message, false)
+      }
+    )
+}
+
+/**
+ * Register callback on register key.
+ *
+ * @param {PublicKeyCredential} publicKey @see https://www.w3.org/TR/webauthn/#publickeycredential
+ * @param {function(PublicKeyCredential)} callback  User callback
+ */
+WebAuthn.prototype._registerCallback = function(publicKey, callback) {
+  let publicKeyCredential = {
+    id: publicKey.id,
+    type: publicKey.type,
+    rawId: this._bufferEncode(publicKey.rawId),
+    response: {
+      /** @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse */
+      clientDataJSON: this._bufferEncode(publicKey.response.clientDataJSON),
+      attestationObject: this._bufferEncode(publicKey.response.attestationObject),
+    },
+  }
+
+  callback(publicKeyCredential)
+}
+
+/**
+ * Authenticate a user.
+ *
+ * @param {PublicKeyCredentialRequestOptions} publicKey  - see https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions
+ * @param {function(PublicKeyCredential)} callback  User callback
+ */
+WebAuthn.prototype.sign = function(publicKey, callback) {
+  let publicKeyCredential = Object.assign({}, publicKey)
+  publicKeyCredential.challenge = this._bufferDecode(this._base64Decode(publicKey.challenge))
+  if (publicKey.allowCredentials) {
+    publicKeyCredential.allowCredentials = this._credentialDecode(publicKey.allowCredentials)
+  }
+
+  var self = this
+  navigator.credentials
+    .get({
+      publicKey: publicKeyCredential,
+    })
+    .then(
+      data => {
+        self._signCallback(data, callback)
+      },
+      error => {
+        self._notify(error.name, error.message, false)
+      }
+    )
+}
+
+/**
+ * Sign callback on authenticate.
+ *
+ * @param {PublicKeyCredential} publicKey @see https://www.w3.org/TR/webauthn/#publickeycredential
+ * @param {function(PublicKeyCredential)} callback  User callback
+ */
+WebAuthn.prototype._signCallback = function(publicKey, callback) {
+  let publicKeyCredential = {
+    id: publicKey.id,
+    type: publicKey.type,
+    rawId: this._bufferEncode(publicKey.rawId),
+    response: {
+      /** @see https://www.w3.org/TR/webauthn/#iface-authenticatorassertionresponse */
+      authenticatorData: this._bufferEncode(publicKey.response.authenticatorData),
+      clientDataJSON: this._bufferEncode(publicKey.response.clientDataJSON),
+      signature: this._bufferEncode(publicKey.response.signature),
+      userHandle: publicKey.response.userHandle
+        ? this._bufferEncode(publicKey.response.userHandle)
+        : null,
+    },
+  }
+
+  callback(publicKeyCredential)
+}
+
+/**
+ * Buffer encode.
+ *
+ * @param {ArrayBuffer} value
+ * @return {string}
+ */
+WebAuthn.prototype._bufferEncode = function(value) {
+  return window.btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
+}
+
+/**
+ * Buffer decode.
+ *
+ * @param {ArrayBuffer} value
+ * @return {string}
+ */
+WebAuthn.prototype._bufferDecode = function(value) {
+  var t = window.atob(value)
+  return Uint8Array.from(t, c => c.charCodeAt(0))
+}
+
+/**
+ * Convert a base64url to a base64 string.
+ *
+ * @param {string} input
+ * @return {string}
+ */
+WebAuthn.prototype._base64Decode = function(input) {
+  // Replace non-url compatible chars with base64 standard chars
+  input = input.replace(/-/g, '+').replace(/_/g, '/')
+
+  // Pad out with standard base64 required padding characters
+  const pad = input.length % 4
+  if (pad) {
+    if (pad === 1) {
+      throw new Error(
+        'InvalidLengthError: Input base64url string is the wrong length to determine padding'
+      )
+    }
+    input += new Array(5 - pad).join('=')
+  }
+
+  return input
+}
+
+/**
+ * Credential decode.
+ *
+ * @param {PublicKeyCredentialDescriptor} credentials
+ * @return {PublicKeyCredentialDescriptor}
+ */
+WebAuthn.prototype._credentialDecode = function(credentials) {
+  var self = this
+  return credentials.map(function(data) {
+    return {
+      id: self._bufferDecode(self._base64Decode(data.id)),
+      type: data.type,
+      transports: data.transports,
+    }
+  })
+}
+
+/**
+ * Test is WebAuthn is supported by this navigator.
+ *
+ * @return {bool}
+ */
+WebAuthn.prototype.webAuthnSupport = function() {
+  return !(
+    window.PublicKeyCredential === undefined ||
+    typeof window.PublicKeyCredential !== 'function' ||
+    typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable !== 'function'
+  )
+}
+
+/**
+ * Get the message in case WebAuthn is not supported.
+ *
+ * @return {string}
+ */
+WebAuthn.prototype.notSupportedMessage = function() {
+  if (
+    !window.isSecureContext &&
+    window.location.hostname !== 'localhost' &&
+    window.location.hostname !== '127.0.0.1'
+  ) {
+    return 'not_secured'
+  }
+  return 'not_supported'
+}
+
+/**
+ * Call the notify callback.
+ *
+ * @param {string} message
+ * @param {bool} isError
+ */
+WebAuthn.prototype._notify = function(message, isError) {
+  if (this._notifyCallback) {
+    this._notifyCallback(message, isError)
+  }
+}
+
+/**
+ * Set the notify callback.
+ *
+ * @param {function(name: string, message: string, isError: bool)} callback
+ */
+WebAuthn.prototype.setNotify = function(callback) {
+  this._notifyCallback = callback
+}
+
+window.WebAuthn = WebAuthn

+ 1 - 1
resources/views/auth/backup_code.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/login.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/passwords/email.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/passwords/reset.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/register.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/two_factor.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 1 - 1
resources/views/auth/usernames/email.blade.php

@@ -1,4 +1,4 @@
-@extends('layouts.app')
+@extends('layouts.auth')
 
 
 @section('content')
 @section('content')
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
     <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">

+ 27 - 0
resources/views/layouts/auth.blade.php

@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="{{ app()->getLocale() }}">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <!-- CSRF Token -->
+    <meta name="csrf-token" content="{{ csrf_token() }}">
+
+    <title>{{ config('app.name', 'Laravel') }}</title>
+
+    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
+    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
+    <link rel="manifest" href="/site.webmanifest">
+    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
+    <meta name="msapplication-TileColor" content="#da532c">
+    <meta name="theme-color" content="#19216C">
+
+    <!-- Styles -->
+    <link href="{{ mix('css/app.css') }}" rel="stylesheet">
+    @yield('webauthn')
+</head>
+<body class="bg-grey-50 antialiased text-grey-900">
+    @yield('content')
+</body>
+</html>

+ 50 - 52
resources/views/nav/nav.blade.php

@@ -1,58 +1,56 @@
 @auth
 @auth
-@if((Auth::user()->two_factor_enabled && session('two_factor_auth')) || (!Auth::user()->two_factor_enabled && !session('two_factor_auth')))
-    <nav class="bg-indigo-900 py-4 shadow">
-        <div class="container flex items-center justify-between flex-wrap">
-            <div class="flex items-center flex-shrink-0 text-white mr-6">
-                <a href="{{ route('aliases.index') }}">
-                    <img class="h-6" alt="AnonAddy Logo" src="/svg/icon-logo.svg">
+<nav class="bg-indigo-900 py-4 shadow">
+    <div class="container flex items-center justify-between flex-wrap">
+        <div class="flex items-center flex-shrink-0 text-white mr-6">
+            <a href="{{ route('aliases.index') }}">
+                <img class="h-6" alt="AnonAddy Logo" src="/svg/icon-logo.svg">
+            </a>
+        </div>
+        <div class="block md:hidden">
+            <button @click="mobileNavActive = !mobileNavActive" class="flex items-center px-3 py-2 border rounded text-indigo-200 border-indigo-400 hover:text-white hover:border-white focus:outline-none">
+            <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
+            </button>
+        </div>
+        <div class="w-full flex-grow md:flex md:items-center md:w-auto" :class="mobileNavActive ? 'block' : 'hidden'">
+            <div class="text-base md:flex-grow">
+                <a href="{{ route('aliases.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('aliases.index') ? 'text-white' : 'text-indigo-100' }}">
+                    Aliases
+                </a>
+                <a href="{{ route('recipients.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('recipients.index') ? 'text-white' : 'text-indigo-100' }}">
+                    Recipients
+                </a>
+                <a href="{{ route('domains.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('domains.index') ? 'text-white' : 'text-indigo-100' }}">
+                    Domains
+                </a>
+                <a href="{{ route('usernames.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('usernames.index') ? 'text-white' : 'text-indigo-100' }}">
+                    Usernames
+                </a>
+                <a href="{{ route('rules.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('rules.index') ? 'text-white' : 'text-indigo-100' }}">
+                    Rules
                 </a>
                 </a>
-            </div>
-            <div class="block md:hidden">
-                <button @click="mobileNavActive = !mobileNavActive" class="flex items-center px-3 py-2 border rounded text-indigo-200 border-indigo-400 hover:text-white hover:border-white focus:outline-none">
-                <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
-                </button>
-            </div>
-            <div class="w-full flex-grow md:flex md:items-center md:w-auto" :class="mobileNavActive ? 'block' : 'hidden'">
-                <div class="text-base md:flex-grow">
-                    <a href="{{ route('aliases.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('aliases.index') ? 'text-white' : 'text-indigo-100' }}">
-                        Aliases
-                    </a>
-                    <a href="{{ route('recipients.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('recipients.index') ? 'text-white' : 'text-indigo-100' }}">
-                        Recipients
-                    </a>
-                    <a href="{{ route('domains.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('domains.index') ? 'text-white' : 'text-indigo-100' }}">
-                        Domains
-                    </a>
-                    <a href="{{ route('usernames.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('usernames.index') ? 'text-white' : 'text-indigo-100' }}">
-                        Usernames
-                    </a>
-                    <a href="{{ route('rules.index') }}" class="block mt-4 md:inline-block md:mt-0 hover:text-white mr-4 {{ Route::currentRouteNamed('rules.index') ? 'text-white' : 'text-indigo-100' }}">
-                        Rules
-                    </a>
 
 
-                    <a href="{{ route('settings.show') }}" class="block md:hidden mt-4 hover:text-white mr-4 {{ Route::currentRouteNamed('settings.show') ? 'text-white' : 'text-indigo-100' }}">
-                        Settings
-                    </a>
-                    <form action="{{ route('logout') }}" method="POST" class="block md:hidden">
-                        {{ csrf_field() }}
-                        <input type="submit" class="bg-transparent block text-indigo-100 mt-4 hover:text-white mr-4" value="{{ __('Logout') }}">
-                    </form>
-                </div>
-                <dropdown class="hidden md:block" username="{{ user()->username }}">
-                    <ul>
-                        <li>
-                            <a href="{{ route('settings.show') }}" class="block px-4 py-2 hover:bg-indigo-500 hover:text-white">Settings</a>
-                        </li>
-                        <li>
-                            <form action="{{ route('logout') }}" method="POST" class="block">
-                                {{ csrf_field() }}
-                                <input type="submit" class="w-full px-4 py-2 bg-transparent hover:bg-indigo-500 hover:text-white cursor-pointer text-left" value="{{ __('Logout') }}">
-                            </form>
-                        </li>
-                    </ul>
-                </dropdown>
+                <a href="{{ route('settings.show') }}" class="block md:hidden mt-4 hover:text-white mr-4 {{ Route::currentRouteNamed('settings.show') ? 'text-white' : 'text-indigo-100' }}">
+                    Settings
+                </a>
+                <form action="{{ route('logout') }}" method="POST" class="block md:hidden">
+                    {{ csrf_field() }}
+                    <input type="submit" class="bg-transparent block text-indigo-100 mt-4 hover:text-white mr-4" value="{{ __('Logout') }}">
+                </form>
             </div>
             </div>
+            <dropdown class="hidden md:block" username="{{ user()->username }}">
+                <ul>
+                    <li>
+                        <a href="{{ route('settings.show') }}" class="block px-4 py-2 hover:bg-indigo-500 hover:text-white">Settings</a>
+                    </li>
+                    <li>
+                        <form action="{{ route('logout') }}" method="POST" class="block">
+                            {{ csrf_field() }}
+                            <input type="submit" class="w-full px-4 py-2 bg-transparent hover:bg-indigo-500 hover:text-white cursor-pointer text-left" value="{{ __('Logout') }}">
+                        </form>
+                    </li>
+                </ul>
+            </dropdown>
         </div>
         </div>
-    </nav>
-@endif
+    </div>
+</nav>
 @endauth
 @endauth

+ 92 - 41
resources/views/settings/show.blade.php

@@ -9,7 +9,7 @@
                 <div class="flex items-center mb-2">
                 <div class="flex items-center mb-2">
                     <span class="rounded-full bg-yellow-400 uppercase px-2 py-1 text-xs font-bold mr-2">Important</span>
                     <span class="rounded-full bg-yellow-400 uppercase px-2 py-1 text-xs font-bold mr-2">Important</span>
                     <div>
                     <div>
-                        2FA enabled successfully. Please <b>make a copy of your backup code below</b>. If you lose your 2FA device you can use this backup code to disable 2FA on your account. <b>This is the only time this code will be displayed, so be sure not to lose it!</b>
+                        2FA enabled successfully. Please <b>make a copy of your backup code below</b>. If you have an old backup code saved <b>you must update it with this one.</b> If you lose your 2FA device you can use this backup code to disable 2FA on your account. <b>This is the only time this code will be displayed, so be sure not to lose it!</b>
                     </div>
                     </div>
                 </div>
                 </div>
                 <pre class="flex p-3 text-grey-900 bg-white border rounded">
                 <pre class="flex p-3 text-grey-900 bg-white border rounded">
@@ -289,8 +289,33 @@
 
 
             </form>
             </form>
 
 
+        </div>
+
+        <div class="mb-4">
+            <h2 class="text-3xl font-bold">
+                Two-Factor Authentication
+            </h2>
+            <p class="text-grey-500">Manage your 2FA options</p>
+        </div>
+
+        <div class="px-6 py-8 md:p-10 bg-white rounded-lg shadow mb-10">
+
+            <div id="two-factor">
+
+                <h3 class="font-bold text-xl">
+                Information
+                </h3>
+
+                <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+
+                <p class="mt-6">
+                    Two-factor authentication, also known as 2FA or multi-factor, adds an extra layer of security to your account beyond your username and password. There are <b>two options for 2FA</b> - Authentication App (e.g. Google Authenticator or another, Aegis, andOTP) or U2F Device Authentication (e.g. YubiKey, SoloKey, Nitrokey).
+                </p>
+
+                <p class="mt-4 pb-16">
+                    When you login with 2FA enabled, you will be prompted to use a security key or enter a OTP (one time passcode) depending on which method you choose below. You can only have one method of 2nd factor authentication enabled at once.
+                </p>
 
 
-            <div id="two-factor" class="pt-16">
                 @if($user->two_factor_enabled)
                 @if($user->two_factor_enabled)
 
 
                     <form method="POST" action="{{ route('settings.2fa_disable') }}">
                     <form method="POST" action="{{ route('settings.2fa_disable') }}">
@@ -299,7 +324,7 @@
                         <div class="mb-6">
                         <div class="mb-6">
 
 
                             <h3 class="font-bold text-xl">
                             <h3 class="font-bold text-xl">
-                                Disable 2 Factor Authentication
+                                Disable Authentication App (TOTP)
                             </h3>
                             </h3>
 
 
                             <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
                             <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
@@ -323,63 +348,89 @@
                         </div>
                         </div>
 
 
                         <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
                         <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
-                            {{ __('Disable 2FA') }}
+                            {{ __('Disable') }}
                         </button>
                         </button>
 
 
                     </form>
                     </form>
 
 
                 @else
                 @else
 
 
+                    @if(App\Facades\Webauthn::enabled($user))
 
 
-                    <div class="mb-6">
+                        <webauthn-keys />
 
 
-                        <h3 class="font-bold text-xl">
-                            Enable 2 Factor Authentication
-                        </h3>
+                    @else
 
 
-                        <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+                        <div class="mb-6">
+
+                            <h3 class="font-bold text-xl">
+                                Enable Authentication App (TOTP)
+                            </h3>
+
+                            <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+
+                            <p class="mt-6">TOTP 2 factor authentication requires the use of Google Authenticator or another compatible app such as Aegis or andOTP (both on F-droid) for Android. Alternatively, you can use the code below. Make sure that you write down your secret code in a safe place.</p>
+
+                            <div>
+                                <img src="{{ $qrCode }}">
+                                <p class="mb-2">Secret: {{ $authSecret }}</p>
+                                <form method="POST" action="{{ route('settings.2fa_regenerate') }}">
+                                    @csrf
+                                    <input type="submit" class="text-indigo-900 bg-transparent cursor-pointer" value="Click here to regenerate your secret key">
+
+                                    @if ($errors->has('regenerate_2fa'))
+                                        <p class="text-red-500 text-xs italic mt-4">
+                                            {{ $errors->first('regenerate_2fa') }}
+                                        </p>
+                                    @endif
+                                </form>
+                            </div>
 
 
-                        <p class="mt-6">2 factor authentication requires the use of Google Authenticator or another compatible app such as Aegis or andOTP (both on F-droid) for Android. Alternatively, you can use the code below. Make sure that you write down your secret code in a safe place.</p>
+                        </div>
 
 
-                        <div>
-                            <img src="{{ $qrCode }}">
-                            <p class="mb-2">Secret: {{ $authSecret }}</p>
-                            <form method="POST" action="{{ route('settings.2fa_regenerate') }}">
-                                @csrf
-                                <input type="submit" class="text-indigo-900 bg-transparent cursor-pointer" value="Click here to regenerate your secret key">
+                        <form method="POST" action="{{ route('settings.2fa_enable') }}">
+                            @csrf
+                            <div class="my-6 flex flex-wrap">
+                                <label for="two_factor_token" class="block text-grey-700 text-sm mb-2">
+                                    {{ __('Verify and Enable') }}:
+                                </label>
+
+                                <div class="block relative w-full">
+                                    <input id="two_factor_token" type="text" class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline" name="two_factor_token" placeholder="123456" />
+                                </div>
 
 
-                                @if ($errors->has('regenerate_2fa'))
+                                @if ($errors->has('two_factor_token'))
                                     <p class="text-red-500 text-xs italic mt-4">
                                     <p class="text-red-500 text-xs italic mt-4">
-                                        {{ $errors->first('regenerate_2fa') }}
+                                        {{ $errors->first('two_factor_token') }}
                                     </p>
                                     </p>
                                 @endif
                                 @endif
-                            </form>
-                        </div>
+                            </div>
+                            <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
+                                {{ __('Verify and Enable') }}
+                            </button>
+                        </form>
 
 
-                    </div>
+                        <div class="pt-16">
 
 
-                    <form method="POST" action="{{ route('settings.2fa_enable') }}">
-                        @csrf
-                        <div class="my-6 flex flex-wrap">
-                            <label for="two_factor_token" class="block text-grey-700 text-sm mb-2">
-                                {{ __('Verify and Enable') }}:
-                            </label>
+                            <h3 class="font-bold text-xl">
+                                Enable Device Authentication (U2F)
+                            </h3>
 
 
-                            <div class="block relative w-full">
-                                <input id="two_factor_token" type="text" class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline" name="two_factor_token" placeholder="123456" />
-                            </div>
+                            <div class="mt-4 w-24 border-b-2 border-grey-200"></div>
+
+                            <p class="my-6">U2F is a standard for universal two-factor authentication tokens. You can use any U2F key such as a Yubikey, Solokey, NitroKey etc.</p>
+
+                            <a
+                            type="button"
+                            href="/webauthn/register"
+                            class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none text-center"
+                            >
+                                Register U2F Device
+                            </a>
 
 
-                            @if ($errors->has('two_factor_token'))
-                                <p class="text-red-500 text-xs italic mt-4">
-                                    {{ $errors->first('two_factor_token') }}
-                                </p>
-                            @endif
                         </div>
                         </div>
-                        <button type="submit" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none">
-                            {{ __('Verify and Enable') }}
-                        </button>
-                    </form>
 
 
+                    @endif
                 @endif
                 @endif
             </div>
             </div>
 
 
@@ -387,9 +438,9 @@
 
 
         <div class="mb-4">
         <div class="mb-4">
             <h2 class="text-3xl font-bold">
             <h2 class="text-3xl font-bold">
-                Pro Settings
+                Other Settings
             </h2>
             </h2>
-            <p class="text-grey-500">Update pro account preferences</p>
+            <p class="text-grey-500">Update your other account preferences</p>
         </div>
         </div>
 
 
         <div class="px-6 py-8 md:p-10 bg-white rounded-lg shadow mb-10">
         <div class="px-6 py-8 md:p-10 bg-white rounded-lg shadow mb-10">

+ 108 - 0
resources/views/vendor/webauthn/authenticate.blade.php

@@ -0,0 +1,108 @@
+@extends('layouts.auth')
+
+@section('content')
+    <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
+        <div class="w-full max-w-lg">
+            <div class="flex flex-col break-words bg-white border border-2 rounded-lg shadow-lg overflow-hidden">
+
+                <div class="px-6 py-8 md:p-10">
+
+                    <h1 class="text-center font-bold text-2xl">
+                        {{ trans('webauthn::messages.auth.title') }}
+                    </h1>
+
+                    <div class="mx-auto my-6 w-24 border-b-2 border-grey-200"></div>
+
+                    <div class="text-sm border-t-8 rounded text-red-800 border-red-600 bg-red-100 px-3 py-4 mb-4 hidden" role="alert" id="error"></div>
+
+                    <div class="text-sm border-t-8 rounded text-green-700 border-green-600 bg-green-100 px-3 py-4 mb-4 hidden" role="alert" id="success">
+                        {{ trans('webauthn::messages.success') }}
+                    </div>
+
+                    <h3>
+                        {{ trans('webauthn::messages.insertKey') }}
+                    </h3>
+
+                    <p class="my-4 text-center">
+                        <img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png" alt=""/>
+                    </p>
+
+                    <p>
+                        {{ trans('webauthn::messages.buttonAdvise') }}
+                        <br />
+                        {{ trans('webauthn::messages.noButtonAdvise') }}
+                    </p>
+
+                    <form method="POST" action="{{ route('webauthn.auth') }}" id="form">
+                        @csrf
+                        <input type="hidden" name="data" id="data" />
+                    </form>
+
+                </div>
+
+                <div class="px-6 md:px-10 py-4 bg-grey-50 border-t border-grey-100 flex flex-wrap justify-between">
+                    <form action="{{ route('logout') }}" method="POST">
+                        @csrf
+                        <input type="submit" class="bg-transparent cursor-pointer no-underline" value="{{ __('Logout') }}">
+                    </form>
+                    <a href="{{ route('login.backup_code.index') }}">Use backup code</a>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        var publicKey = {!! json_encode($publicKey) !!};
+
+        var errors = {
+            key_already_used: "{{ trans('webauthn::errors.key_already_used') }}",
+            key_not_allowed: "{{ trans('webauthn::errors.key_not_allowed') }}",
+            not_secured: "{{ trans('webauthn::errors.not_secured') }}",
+            not_supported: "{{ trans('webauthn::errors.not_supported') }}",
+        };
+
+        function errorMessage(name, message) {
+            switch (name) {
+            case 'InvalidStateError':
+            return errors.key_already_used;
+            case 'NotAllowedError':
+            return errors.key_not_allowed;
+            default:
+            return message;
+            }
+        }
+
+        function error(message) {
+            document.getElementById("error").innerHTML = message;
+            document.getElementById("error").classList.remove("hidden");
+        }
+
+        var webauthn = new WebAuthn((name, message) => {
+            error(errorMessage(name, message));
+        });
+
+        if (! webauthn.webAuthnSupport()) {
+            switch (webauthn.notSupportedMessage()) {
+            case 'not_secured':
+                error(errors.not_secured);
+                break;
+            case 'not_supported':
+                error(errors.not_supported);
+                break;
+            }
+        }
+
+        webauthn.sign(
+            publicKey,
+            function (datas) {
+                document.getElementById("success").classList.remove("hidden");
+                document.getElementById("data").value = JSON.stringify(datas);
+                document.getElementById("form").submit();
+            }
+        );
+    </script>
+@endsection
+
+@section('webauthn')
+    <script src="{!! secure_asset('js/webauthn.js') !!}"></script>
+@endsection

+ 135 - 0
resources/views/vendor/webauthn/register.blade.php

@@ -0,0 +1,135 @@
+@extends('layouts.auth')
+
+@section('content')
+    <div class="p-6 bg-indigo-900 min-h-screen flex justify-center items-center">
+        <div class="w-full max-w-lg">
+            <div class="flex flex-col break-words bg-white border border-2 rounded-lg shadow-lg overflow-hidden">
+
+                <div class="px-6 py-8 md:p-10">
+
+                    <h1 class="text-center font-bold text-2xl">
+                        {{ trans('webauthn::messages.register.title') }}
+                    </h1>
+
+                    <div class="mx-auto my-6 w-24 border-b-2 border-grey-200"></div>
+
+                    <div class="text-sm border-t-8 rounded text-red-800 border-red-600 bg-red-100 px-3 py-4 mb-4 hidden" role="alert" id="error"></div>
+
+                    <div class="text-sm border-t-8 rounded text-green-700 border-green-600 bg-green-100 px-3 py-4 mb-4 hidden" role="alert" id="success">
+                        {{ trans('webauthn::messages.success') }}
+                    </div>
+
+                    <h3>
+                        {{ trans('webauthn::messages.insertKey') }}
+                    </h3>
+
+                    <p class="my-4 text-center">
+                        <img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png" alt=""/>
+                    </p>
+
+                    <p>
+                        {{ trans('webauthn::messages.buttonAdvise') }}
+                        <br />
+                        {{ trans('webauthn::messages.noButtonAdvise') }}
+                    </p>
+
+                    <form method="POST" class="mt-8" action="{{ route('webauthn.create') }}" id="form">
+                        @csrf
+                        <input type="hidden" name="register" id="register">
+
+                        <label for="name" class="block text-grey-700 text-sm mb-2">
+                            Name:
+                        </label>
+                        <input type="text" class="appearance-none bg-grey-100 rounded w-full p-3 text-grey-700 focus:shadow-outline" name="name" id="name" placeholder="Yubikey" required autofocus>
+
+                        @if ($errors->has('name'))
+                            <p class="text-red-500 text-xs italic mt-4">
+                                {{ $errors->first('name') }}
+                            </p>
+                        @endif
+                    </form>
+
+                </div>
+
+                <div class="px-6 md:px-10 py-4 bg-grey-50 border-t border-grey-100 flex flex-wrap items-center">
+                    <button onclick="registerDevice()" class="bg-cyan-400 w-full hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto">
+                        Add Device
+                    </button>
+                </div>
+            </div>
+            <p class="w-full text-xs text-center text-indigo-100 mt-6">
+                Changed your mind?
+                <a class="text-white hover:text-indigo-50 no-underline" href="{{ route('settings.show') }}">
+                    {{ trans('webauthn::messages.cancel') }}
+                </a>
+            </p>
+        </div>
+    </div>
+
+    <script>
+        var publicKey = {!! json_encode($publicKey) !!};
+
+        var errors = {
+            key_already_used: "{{ trans('webauthn::errors.key_already_used') }}",
+            key_not_allowed: "{{ trans('webauthn::errors.key_not_allowed') }}",
+            not_secured: "{{ trans('webauthn::errors.not_secured') }}",
+            not_supported: "{{ trans('webauthn::errors.not_supported') }}",
+        };
+
+        function errorMessage(name, message) {
+            switch (name) {
+            case 'InvalidStateError':
+                return errors.key_already_used;
+            case 'NotAllowedError':
+                return errors.key_not_allowed;
+            default:
+                return message;
+            }
+        }
+
+        function error(message) {
+            document.getElementById("error").innerHTML = message;
+            document.getElementById("error").classList.remove("hidden");
+        }
+
+        var webauthn = new WebAuthn((name, message) => {
+            error(errorMessage(name, message));
+        });
+
+        if (! webauthn.webAuthnSupport()) {
+            switch (webauthn.notSupportedMessage()) {
+                case 'not_secured':
+                error(errors.not_secured);
+                break;
+                case 'not_supported':
+                error(errors.not_supported);
+                break;
+            }
+        }
+
+        function registerDevice() {
+
+            if(document.getElementById("name").value === '') {
+                return error('A device name is required');
+            }
+
+            if(document.getElementById("name").value.length > 50) {
+                return error('The device name may not be greater than 50 characters.');
+            }
+
+            webauthn.register(
+                publicKey,
+                function (datas) {
+                    document.getElementById("success").classList.remove("hidden");
+                    document.getElementById("register").value = JSON.stringify(datas);
+                    document.getElementById("form").submit();
+                }
+            );
+        }
+
+    </script>
+@endsection
+
+@section('webauthn')
+    <script src="{!! secure_asset('js/webauthn.js') !!}"></script>
+@endsection

+ 12 - 2
routes/web.php

@@ -23,7 +23,17 @@ Route::post('/login/2fa', 'Auth\TwoFactorAuthController@authenticateTwoFactor')-
 Route::get('/login/backup-code', 'Auth\BackupCodeController@index')->name('login.backup_code.index');
 Route::get('/login/backup-code', 'Auth\BackupCodeController@index')->name('login.backup_code.index');
 Route::post('/login/backup-code', 'Auth\BackupCodeController@login')->name('login.backup_code.login');
 Route::post('/login/backup-code', 'Auth\BackupCodeController@login')->name('login.backup_code.login');
 
 
-Route::middleware(['auth', 'verified', '2fa'])->group(function () {
+Route::group([
+    'middleware' => config('webauthn.middleware', []),
+    'domain' => config('webauthn.domain', null),
+    'prefix' => config('webauthn.prefix', 'webauthn'),
+], function () {
+    Route::get('keys', 'Auth\WebauthnController@index')->name('webauthn.index');
+    Route::post('register', 'Auth\WebauthnController@create')->name('webauthn.create');
+    Route::delete('{id}', 'Auth\WebauthnController@destroy')->name('webauthn.destroy');
+});
+
+Route::middleware(['auth', 'verified', '2fa', 'webauthn'])->group(function () {
     Route::get('/', 'ShowAliasController@index')->name('aliases.index');
     Route::get('/', 'ShowAliasController@index')->name('aliases.index');
 
 
     Route::get('/recipients', 'ShowRecipientController@index')->name('recipients.index');
     Route::get('/recipients', 'ShowRecipientController@index')->name('recipients.index');
@@ -41,7 +51,7 @@ Route::middleware(['auth', 'verified', '2fa'])->group(function () {
 
 
 
 
 Route::group([
 Route::group([
-    'middleware' => ['auth', '2fa'],
+    'middleware' => ['auth', '2fa', 'webauthn'],
     'prefix' => 'settings'
     'prefix' => 'settings'
 ], function () {
 ], function () {
     Route::get('/', 'SettingController@show')->name('settings.show');
     Route::get('/', 'SettingController@show')->name('settings.show');

+ 1 - 0
webpack.mix.js

@@ -2,6 +2,7 @@ let mix = require('laravel-mix')
 
 
 mix
 mix
   .js('resources/js/app.js', 'public/js')
   .js('resources/js/app.js', 'public/js')
+  .js('resources/js/webauthn.js', 'public/js')
   .postCss('resources/css/app.css', 'public/css')
   .postCss('resources/css/app.css', 'public/css')
   .options({
   .options({
     postCss: [
     postCss: [

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio