Updated to vue 3

This commit is contained in:
Will Browning 2022-08-23 09:58:25 +01:00
parent 37d6af9719
commit 7d3219cdb4
43 changed files with 2061 additions and 1678 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

547
composer.lock generated
View file

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

View file

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

1533
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

25
resources/css/app.css vendored
View file

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

95
resources/js/app.js vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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