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
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)!

View file

@ -81,7 +81,6 @@ class ReceiveEmail extends Command
$this->size = $this->option('size') / ($recipientCount ? $recipientCount : 1);
foreach ($recipients as $recipient) {
// Handle bounces
if ($this->option('sender') === 'MAILER-DAEMON') {
$this->handleBounce($recipient['email']);
@ -145,7 +144,6 @@ class ReceiveEmail extends Command
$verifiedRecipient = $user->getVerifiedRecipientByEmail($this->senderFrom);
if ($validEmailDestination && $verifiedRecipient?->can_reply_send) {
// 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')) {
// Notify user and exit
@ -295,7 +293,6 @@ class ReceiveEmail extends Command
// Verify queue ID
if (isset($dsn['X-postfix-queue-id'])) {
// First check in DB
$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,
'inline_encryption' => false,
'protected_headers' => false,
'inline_encryption' => false,
'protected_headers' => false,
'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);
$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)) {
$recipient->sendUsernameReminderNotification();

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\StorePersonalAccessTokenRequest;
use App\Http\Resources\PersonalAccessTokenResource;
use chillerlan\QRCode\QRCode;
class PersonalAccessTokenController extends Controller
{
@ -24,10 +25,12 @@ class PersonalAccessTokenController extends Controller
}
$token = user()->createToken($request->name, ['*'], $expiration);
$accessToken = explode('|', $token->plainTextToken, 2)[1];
return [
'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,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::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,
'2fa' => \App\Http\Middleware\VerifyTwoFactorAuth::class,
'webauthn' => \App\Http\Middleware\VerifyWebauthn::class,

View file

@ -19,6 +19,6 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
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);
});
break;
// regex preg_match?
// regex preg_match?
}
}

View file

@ -10,6 +10,7 @@
"php": "^8.0.2",
"asbiin/laravel-webauthn": "^3.0.0",
"bacon/bacon-qr-code": "^2.0",
"chillerlan/php-qrcode": "^4.3",
"doctrine/dbal": "^3.0",
"guzzlehttp/guzzle": "^7.2",
"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",
"This file is @generated automatically"
],
"content-hash": "e82a971bfd5ab4ba0fa31965a1c6ca60",
"content-hash": "6a781a6eb7831ca8568ca458163ccb56",
"packages": [
{
"name": "asbiin/laravel-webauthn",
"version": "3.2.0",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/asbiin/laravel-webauthn.git",
"reference": "747df5ab7bf0f88bbb36f11c0f281464d6c29d0b"
"reference": "05610549427974bf8a3f0cc32a58e4050d9f7792"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/747df5ab7bf0f88bbb36f11c0f281464d6c29d0b",
"reference": "747df5ab7bf0f88bbb36f11c0f281464d6c29d0b",
"url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/05610549427974bf8a3f0cc32a58e4050d9f7792",
"reference": "05610549427974bf8a3f0cc32a58e4050d9f7792",
"shasum": ""
},
"require": {
@ -92,7 +92,7 @@
"type": "github"
}
],
"time": "2022-07-30T09:08:07+00:00"
"time": "2022-08-20T17:56:48+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -217,16 +217,16 @@
},
{
"name": "brick/math",
"version": "0.10.1",
"version": "0.10.2",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "de846578401f4e58f911b3afeb62ced56365ed87"
"reference": "459f2781e1a08d52ee56b0b1444086e038561e3f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/de846578401f4e58f911b3afeb62ced56365ed87",
"reference": "de846578401f4e58f911b3afeb62ced56365ed87",
"url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f",
"reference": "459f2781e1a08d52ee56b0b1444086e038561e3f",
"shasum": ""
},
"require": {
@ -261,7 +261,7 @@
],
"support": {
"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": [
{
@ -269,7 +269,149 @@
"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",
@ -488,16 +630,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.4.0",
"version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "118a360e9437e88d49024f36283c8bcbd76105f5"
"reference": "22de295f10edbe00df74f517612f1fbd711131e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/118a360e9437e88d49024f36283c8bcbd76105f5",
"reference": "118a360e9437e88d49024f36283c8bcbd76105f5",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/22de295f10edbe00df74f517612f1fbd711131e2",
"reference": "22de295f10edbe00df74f517612f1fbd711131e2",
"shasum": ""
},
"require": {
@ -579,7 +721,7 @@
],
"support": {
"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": [
{
@ -595,7 +737,7 @@
"type": "tidelift"
}
],
"time": "2022-08-06T20:35:57+00:00"
"time": "2022-08-21T14:21:06+00:00"
},
{
"name": "doctrine/deprecations",
@ -1695,16 +1837,16 @@
},
{
"name": "laravel/framework",
"version": "v9.24.0",
"version": "v9.25.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "053840f579cf01d353d81333802afced79b1c0af"
"reference": "e8af8c2212e3717757ea7f459a655a2e9e771109"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/053840f579cf01d353d81333802afced79b1c0af",
"reference": "053840f579cf01d353d81333802afced79b1c0af",
"url": "https://api.github.com/repos/laravel/framework/zipball/e8af8c2212e3717757ea7f459a655a2e9e771109",
"reference": "e8af8c2212e3717757ea7f459a655a2e9e771109",
"shasum": ""
},
"require": {
@ -1871,7 +2013,7 @@
"issues": "https://github.com/laravel/framework/issues",
"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",
@ -2316,16 +2458,16 @@
},
{
"name": "league/flysystem",
"version": "3.2.0",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a"
"reference": "81aea9e5217084c7850cd36e1587ee4aad721c6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/ed0ecc7f9b5c2f4a9872185846974a808a3b052a",
"reference": "ed0ecc7f9b5c2f4a9872185846974a808a3b052a",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/81aea9e5217084c7850cd36e1587ee4aad721c6b",
"reference": "81aea9e5217084c7850cd36e1587ee4aad721c6b",
"shasum": ""
},
"require": {
@ -2386,7 +2528,7 @@
],
"support": {
"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": [
{
@ -2402,7 +2544,7 @@
"type": "tidelift"
}
],
"time": "2022-07-26T07:26:36+00:00"
"time": "2022-08-14T20:48:34+00:00"
},
{
"name": "league/mime-type-detection",
@ -7269,16 +7411,16 @@
},
{
"name": "web-auth/cose-lib",
"version": "v4.0.5",
"version": "v4.0.6",
"source": {
"type": "git",
"url": "https://github.com/web-auth/cose-lib.git",
"reference": "2fe6c0d35136d75bc538372a317ca5df5a75ce73"
"reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/web-auth/cose-lib/zipball/2fe6c0d35136d75bc538372a317ca5df5a75ce73",
"reference": "2fe6c0d35136d75bc538372a317ca5df5a75ce73",
"url": "https://api.github.com/repos/web-auth/cose-lib/zipball/d72032843c97ba40edb682dfcec0b787b25cbe6f",
"reference": "d72032843c97ba40edb682dfcec0b787b25cbe6f",
"shasum": ""
},
"require": {
@ -7338,7 +7480,7 @@
],
"support": {
"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": [
{
@ -7350,7 +7492,7 @@
"type": "patreon"
}
],
"time": "2022-08-04T16:48:04+00:00"
"time": "2022-08-16T14:04:27+00:00"
},
{
"name": "web-auth/metadata-service",
@ -8270,16 +8412,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.9.5",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
"reference": "4465d70ba776806857a1ac2a6f877e582445ff36"
"reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4465d70ba776806857a1ac2a6f877e582445ff36",
"reference": "4465d70ba776806857a1ac2a6f877e582445ff36",
"url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe",
"reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe",
"shasum": ""
},
"require": {
@ -8289,7 +8431,7 @@
"ext-json": "*",
"ext-tokenizer": "*",
"php": "^7.4 || ^8.0",
"php-cs-fixer/diff": "^2.0",
"sebastian/diff": "^4.0",
"symfony/console": "^5.4 || ^6.0",
"symfony/event-dispatcher": "^5.4 || ^6.0",
"symfony/filesystem": "^5.4 || ^6.0",
@ -8347,7 +8489,7 @@
"description": "A tool to automatically fix PHP code style",
"support": {
"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": [
{
@ -8355,7 +8497,7 @@
"type": "github"
}
],
"time": "2022-07-22T08:43:51+00:00"
"time": "2022-08-17T22:13:10+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@ -8738,304 +8880,25 @@
},
"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",
"version": "9.2.15",
"version": "9.2.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
"reference": "2593003befdcc10db5e213f9f28814f5aa8ac073"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073",
"reference": "2593003befdcc10db5e213f9f28814f5aa8ac073",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0",
"nikic/php-parser": "^4.14",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@ -9084,7 +8947,7 @@
],
"support": {
"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": [
{
@ -9092,7 +8955,7 @@
"type": "github"
}
],
"time": "2022-03-07T09:28:20+00:00"
"time": "2022-08-20T05:26:47+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -9337,16 +9200,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.21",
"version": "9.5.23",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1"
"reference": "888556852e7e9bbeeedb9656afe46118765ade34"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1",
"reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34",
"reference": "888556852e7e9bbeeedb9656afe46118765ade34",
"shasum": ""
},
"require": {
@ -9361,7 +9224,6 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpspec/prophecy": "^1.12.1",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
@ -9379,9 +9241,6 @@
"sebastian/type": "^3.0",
"sebastian/version": "^3.0.2"
},
"require-dev": {
"phpspec/prophecy-phpunit": "^2.0.1"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*"
@ -9423,7 +9282,7 @@
],
"support": {
"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": [
{
@ -9435,7 +9294,7 @@
"type": "github"
}
],
"time": "2022-06-19T12:14:25+00:00"
"time": "2022-08-22T14:01:36+00:00"
},
{
"name": "pimple/pimple",
@ -10886,16 +10745,16 @@
},
{
"name": "spatie/ray",
"version": "1.35.0",
"version": "1.36.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ray.git",
"reference": "7196737c17718264aef9e446b773ee490c1563dd"
"reference": "4a4def8cda4806218341b8204c98375aa8c34323"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/ray/zipball/7196737c17718264aef9e446b773ee490c1563dd",
"reference": "7196737c17718264aef9e446b773ee490c1563dd",
"url": "https://api.github.com/repos/spatie/ray/zipball/4a4def8cda4806218341b8204c98375aa8c34323",
"reference": "4a4def8cda4806218341b8204c98375aa8c34323",
"shasum": ""
},
"require": {
@ -10945,7 +10804,7 @@
],
"support": {
"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": [
{
@ -10957,7 +10816,7 @@
"type": "other"
}
],
"time": "2022-08-09T14:35:12+00:00"
"time": "2022-08-11T14:04:18+00:00"
},
{
"name": "symfony/filesystem",

View file

@ -46,7 +46,7 @@ return [
/*
* Forbid user to reuse One Time Passwords.
*/
'forbid_old_passwords' => false,
'forbid_old_passwords' => true,
/*
* 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"
},
"dependencies": {
"@headlessui/vue": "^1.6.7",
"@kyvg/vue3-notification": "^2.3.4",
"@vueform/multiselect": "^2.3.2",
"autoprefixer": "^10.4.1",
"axios": "^0.26.0",
"axios": "^0.27.0",
"cross-env": "^7.0.3",
"dayjs": "^1.10.4",
"laravel-mix": "^6.0.11",
"lodash": "^4.17.20",
"portal-vue": "^2.1.7",
"postcss": "^8.4.5",
"postcss-import": "^14.0.0",
"resolve-url-loader": "^5.0.0",
"tailwindcss": "^3.0.11",
"tippy.js": "^6.2.7",
"v-clipboard": "^2.2.3",
"vue": "^2.6.12",
"vue-good-table": "^2.21.3",
"vue-loader": "^15.9.6",
"vue-multiselect": "^2.1.6",
"vue-notification": "^1.3.20",
"v-clipboard": "github:euvl/v-clipboard",
"vue": "^3.0.0",
"vue-good-table-next": "^0.2.1",
"vue-loader": "^17.0.0",
"vue-template-compiler": "^2.6.12",
"vuedraggable": "^2.24.2"
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"css-loader": "^6.0.0",
"husky": "^7.0.0",
"lint-staged": "^12.0.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.0",
"prettier": "^2.2.1"
},
"lint-staged": {

View file

@ -26,7 +26,7 @@
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<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="SESSION_DRIVER" value="array"/>
<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;
}
.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;
}
.multiselect .multiselect__tag-icon:focus,
.multiselect .multiselect__tag-icon:hover {
@apply bg-indigo-400;
.multiselect .multiselect-option.is-selected {
@apply bg-indigo-100 text-indigo-900;
}
.multiselect .multiselect__option--highlight {
@apply bg-indigo-500;
}
.multiselect .multiselect__option--selected.multiselect__option--highlight {
.multiselect .multiselect-option.is-selected:hover {
@apply bg-red-500;
}

95
resources/js/app.js vendored
View file

@ -9,59 +9,58 @@ dayjs.extend(advancedFormat)
dayjs.extend(relativeTime)
dayjs.extend(utc)
import Vue from 'vue'
import { createApp } from 'vue'
import PortalVue from 'portal-vue'
import Clipboard from 'v-clipboard'
import Notifications from 'vue-notification'
import VueGoodTablePlugin from 'vue-good-table'
import Clipboard from 'v-clipboard/src'
import Notifications from '@kyvg/vue3-notification'
import VueGoodTablePlugin from 'vue-good-table-next'
Vue.use(PortalVue)
Vue.use(Clipboard)
Vue.use(Notifications)
Vue.use(VueGoodTablePlugin)
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',
const app = createApp({
data() {
return {
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>
<portal to="modals">
<div
v-if="showModal"
class="fixed inset-0 flex justify-center"
:class="overflow ? 'overflow-auto' : 'items-center'"
>
<transition
@before-leave="backdropLeaving = true"
@after-leave="backdropLeaving = false"
enter-active-class="transition-all transition-fast ease-out-quad"
leave-active-class="transition-all transition-medium ease-in-quad"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
appear
<TransitionRoot as="template" :show="open">
<Dialog as="div" class="relative z-10" @close="open = false">
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div v-if="showBackdrop">
<div
class="inset-0 bg-black opacity-25"
:class="overflow ? 'fixed pointer-events-none' : 'absolute'"
@click="close"
></div>
</div>
</transition>
<div class="fixed inset-0 bg-grey-500 bg-opacity-75 transition-opacity" />
</TransitionChild>
<transition
@before-leave="cardLeaving = true"
@after-leave="cardLeaving = false"
enter-active-class="transition-all transition-fast ease-out-quad"
leave-active-class="transition-all transition-medium ease-in-quad"
enter-class="opacity-0 scale-70"
enter-to-class="opacity-100 scale-100"
leave-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-70"
appear
>
<div v-if="showContent" class="relative">
<slot></slot>
<div class="fixed z-10 inset-0 overflow-y-auto">
<div
class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<DialogPanel
class="relative bg-white rounded-lg px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:w-full sm:p-6"
:class="maxWidth ? maxWidth : 'sm:max-w-lg'"
>
<div class="mt-3 text-center sm:mt-0 sm:text-left">
<DialogTitle
as="h2"
class="text-2xl leading-tight font-medium text-grey-900 border-b-2 border-grey-100 pb-4"
>
<slot name="title"></slot>
</DialogTitle>
<div class="mt-2">
<slot name="content"></slot>
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</transition>
</div>
</portal>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
export default {
props: {
open: {
type: Boolean,
required: true,
},
overflow: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showModal: false,
showBackdrop: false,
showContent: false,
backdropLeaving: false,
cardLeaving: false,
}
},
created() {
const onEscape = e => {
if (this.open && e.keyCode === 27) {
this.close()
}
}
document.addEventListener('keydown', onEscape)
this.$once('hook:destroyed', () => {
document.removeEventListener('keydown', onEscape)
})
},
watch: {
open: {
handler: function (newValue) {
if (newValue) {
this.show()
} else {
this.close()
}
},
immediate: true,
},
leaving(newValue) {
if (newValue === false) {
this.showModal = false
this.$emit('close')
}
},
},
computed: {
leaving() {
return this.backdropLeaving || this.cardLeaving
},
},
methods: {
show() {
this.showModal = true
this.showBackdrop = true
this.showContent = true
},
close() {
this.showBackdrop = false
this.showContent = false
},
props: ['open', 'maxWidth'],
components: {
Dialog,
DialogPanel,
DialogTitle,
TransitionChild,
TransitionRoot,
},
}
</script>
<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>
<div class="relative flex justify-end items-center" @keydown.escape="isOpen = false">
<button
ref="openOptions"
@click="isOpen = !isOpen"
:aria-expanded="isOpen"
id="project-options-menu-0"
aria-has-popup="true"
type="button"
class="w-8 h-8 bg-white inline-flex items-center justify-center text-grey-400 rounded-full hover:text-grey-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
>
<span class="sr-only">Open options</span>
<icon
name="more"
class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
aria-hidden="true"
/>
</button>
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton class="flex items-center text-grey-400 hover:text-grey-600 focus:outline-none">
<span class="sr-only">Open options</span>
<icon
name="more"
class="block w-6 h-6 text-grey-300 fill-current cursor-pointer outline-none"
aria-hidden="true"
/>
</MenuButton>
</div>
<transition
enter-active-class="transition ease-out duration-100"
enter-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-show="isOpen"
class="mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg z-10 bg-white ring-1 ring-black ring-opacity-5 divide-y divide-grey-200"
role="menu"
aria-orientation="vertical"
aria-labelledby="project-options-menu-0"
<MenuItems
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10"
>
<slot></slot>
</div>
<div class="py-1">
<slot></slot>
</div>
</MenuItems>
</transition>
</div>
</Menu>
</template>
<script>
export default {
data() {
return {
isOpen: false,
}
},
created() {
window.addEventListener('click', this.close)
},
beforeDestroy() {
window.removeEventListener('click', this.close)
},
methods: {
close(e) {
if (!this.$refs.openOptions.contains(e.target)) {
this.isOpen = false
}
},
},
}
<script setup>
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
</script>

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@
/>
<icon
v-if="search"
@click.native="search = ''"
@click="search = ''"
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"
/>
@ -146,9 +146,9 @@
<vue-good-table
v-if="initialAliases.length"
@on-search="debounceToolips"
@on-page-change="debounceToolips"
@on-per-page-change="debounceToolips"
v-on:search="debounceToolips"
v-on:page-change="debounceToolips"
v-on:per-page-change="debounceToolips"
:columns="columns"
:rows="rows"
:search-options="{
@ -169,10 +169,10 @@
}"
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!
</div>
<template slot="table-row" slot-scope="props">
</template>
<template #table-row="props">
<span v-if="props.column.field == 'created_at'" class="flex items-center">
<span
:class="`bg-${getAliasStatus(props.row).colour}-100`"
@ -187,8 +187,8 @@
</span>
<span
class="tooltip outline-none text-sm whitespace-nowrap"
:data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
>{{ props.row.created_at | timeAgo }}
:data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
>{{ $filters.timeAgo(props.row.created_at) }}
</span>
</span>
<span v-else-if="props.column.field == 'email'" class="block">
@ -199,10 +199,10 @@
v-clipboard:success="clipboardSuccess"
v-clipboard:error="clipboardError"
><span class="font-semibold text-indigo-800">{{
getAliasLocalPart(props.row) | truncate(60)
$filters.truncate(getAliasLocalPart(props.row), 60)
}}</span
><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>
<div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
@ -220,22 +220,22 @@
<icon
name="close"
class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
@click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
@click="aliasIdToEdit = aliasDescriptionToEdit = ''"
/>
<icon
name="save"
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 v-else-if="props.row.description" class="flex items-center">
<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>
<icon
name="edit"
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)
"
/>
@ -279,7 +279,7 @@
<icon
name="edit"
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
@ -311,16 +311,18 @@
<span v-else class="flex items-center justify-center outline-none" tabindex="-1">
<more-options>
<div role="none">
<span
@click="openSendFromModal(props.row)"
class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
role="menuitem"
>
<icon name="send" class="block mr-3 w-5 h-5 text-grey-300 outline-none" />
Send From
</span>
<MenuItem>
<span
@click="openSendFromModal(props.row)"
class="group cursor-pointer flex items-center px-4 py-3 text-sm text-grey-700 hover:bg-grey-100 hover:text-grey-900"
role="menuitem"
>
<icon name="send" class="block mr-3 w-5 h-5 text-grey-300 outline-none" />
Send From
</span>
</MenuItem>
</div>
<div v-if="props.row.deleted_at" role="none">
<MenuItem v-if="props.row.deleted_at">
<span
@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"
@ -332,8 +334,8 @@
/>
Restore
</span>
</div>
<div v-else role="none">
</MenuItem>
<MenuItem v-else>
<span
@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"
@ -345,8 +347,8 @@
/>
Delete
</span>
</div>
<div role="none">
</MenuItem>
<MenuItem>
<span
@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"
@ -358,7 +360,7 @@
/>
Forget
</span>
</div>
</MenuItem>
</more-options>
</span>
</template>
@ -411,12 +413,8 @@
</div>
<Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Create new alias
</h2>
<template v-slot:title> Create new alias </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
Other aliases e.g. alias@{{ subdomain }} can also be created automatically when they
receive their first email.
@ -524,6 +522,8 @@
<multiselect
id="alias_recipient_ids"
v-model="generateAliasRecipientIds"
mode="tags"
value-prop="id"
:options="recipientOptions"
:multiple="true"
:close-on-select="true"
@ -533,8 +533,6 @@
placeholder="Select recipient(s) (optional)..."
label="email"
track-by="email"
:preselect-first="false"
:show-labels="false"
>
</multiselect>
@ -555,33 +553,29 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Update Alias Recipients
</h2>
<template v-slot:title> Update Alias Recipients </template>
<template v-slot:content>
<p class="my-4 text-grey-700">
Select the recipients for this alias. You can choose multiple recipients. Leave it empty
if you would like to use the default recipient.
</p>
<multiselect
v-model="aliasRecipientsToEdit"
mode="tags"
value-prop="id"
:options="recipientOptions"
:multiple="true"
:close-on-select="true"
:clear-on-select="false"
:searchable="true"
:max="10"
placeholder="Select recipients"
placeholder="Select recipient(s)"
label="email"
track-by="email"
:preselect-first="false"
:show-labels="false"
>
</multiselect>
<div class="mt-6">
@ -602,16 +596,12 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="restoreAliasModalOpen" @close="closeRestoreModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Restore alias
</h2>
<template v-slot:title> Restore alias </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
Are you sure you want to restore this alias? Once restored it will be
<b>able to receive emails again</b>.
@ -634,16 +624,12 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Delete alias
</h2>
<template v-slot:title> Delete alias </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
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,
@ -668,16 +654,12 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="forgetAliasModalOpen" @close="closeForgetModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Forget alias
</h2>
<template v-slot:title> Forget alias </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
Are you sure you want to forget <b class="break-words">{{ aliasToForget.email }}</b
>? Forgetting an alias will disassociate it from your account.
@ -705,16 +687,12 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="sendFromAliasModalOpen" @close="closeSendFromModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Send from alias
</h2>
<template v-slot:title> Send from alias </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
Use this to automatically create the correct address to send an email to in order to send
an <b>email from this alias</b>.
@ -811,7 +789,7 @@
Close
</button>
</div>
</div>
</template>
</Modal>
</div>
</template>
@ -824,7 +802,8 @@ import { roundArrow } from 'tippy.js'
import 'tippy.js/dist/svg-arrow.css'
import 'tippy.js/dist/tippy.css'
import tippy from 'tippy.js'
import Multiselect from 'vue-multiselect'
import Multiselect from '@vueform/multiselect'
import { MenuItem } from '@headlessui/vue'
export default {
props: {
@ -886,6 +865,7 @@ export default {
Toggle,
Multiselect,
MoreOptions,
MenuItem,
},
data() {
return {
@ -1134,7 +1114,7 @@ export default {
openAliasRecipientsModal(alias) {
this.editAliasRecipientsModalOpen = true
this.recipientsAliasToEdit = alias
this.aliasRecipientsToEdit = alias.recipients
this.aliasRecipientsToEdit = _.map(alias.recipients, recipient => recipient.id)
},
closeAliasRecipientsModal() {
this.editAliasRecipientsModalOpen = false
@ -1149,7 +1129,7 @@ export default {
'/api/v1/alias-recipients',
JSON.stringify({
alias_id: this.recipientsAliasToEdit.id,
recipient_ids: _.map(this.aliasRecipientsToEdit, recipient => recipient.id),
recipient_ids: this.aliasRecipientsToEdit,
}),
{
headers: { 'Content-Type': 'application/json' },
@ -1157,7 +1137,9 @@ export default {
)
.then(response => {
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.editAliasRecipientsLoading = false
@ -1362,4 +1344,4 @@ export default {
}
</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
v-if="search"
@click.native="search = ''"
@click="search = ''"
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"
/>
@ -34,7 +34,7 @@
<vue-good-table
v-if="initialDomains.length"
@on-search="debounceToolips"
v-on:search="debounceToolips"
:columns="columns"
:rows="rows"
:search-options="{
@ -48,15 +48,15 @@
}"
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!
</div>
<template slot="table-row" slot-scope="props">
</template>
<template #table-row="props">
<span
v-if="props.column.field == 'created_at'"
class="tooltip outline-none text-sm"
:data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
>{{ props.row.created_at | timeAgo }}
:data-tippy-content="$filters.formatDate(rows[props.row.originalIndex].created_at)"
>{{ $filters.timeAgo(props.row.created_at) }}
</span>
<span v-else-if="props.column.field == 'domain'">
<span
@ -65,7 +65,7 @@
v-clipboard="() => rows[props.row.originalIndex].domain"
v-clipboard:success="clipboardSuccess"
v-clipboard:error="clipboardError"
>{{ props.row.domain | truncate(30) }}</span
>{{ $filters.truncate(props.row.domain, 30) }}</span
>
</span>
<span v-else-if="props.column.field == 'description'">
@ -86,20 +86,20 @@
<icon
name="close"
class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
@click.native="domainIdToEdit = domainDescriptionToEdit = ''"
@click="domainIdToEdit = domainDescriptionToEdit = ''"
/>
<icon
name="save"
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 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
name="edit"
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)
"
/>
@ -108,24 +108,24 @@
<icon
name="plus"
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>
</span>
<span v-else-if="props.column.field === '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
name="edit"
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 class="flex justify-center" v-else>
<icon
name="plus"
class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
@click.native="openDomainDefaultRecipientModal(props.row)"
@click="openDomainDefaultRecipientModal(props.row)"
/>
</div>
</span>
@ -234,7 +234,7 @@
<icon
name="trash"
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>
</template>
@ -265,12 +265,9 @@
</div>
<Modal :open="addDomainModalOpen" @close="closeCheckRecordsModal">
<div v-if="!domainToCheck" class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Add new domain
</h2>
<template v-if="!domainToCheck" v-slot:title> Add new domain </template>
<template v-else v-slot:title> Check DNS records </template>
<template v-if="!domainToCheck" v-slot:content>
<p class="mt-4 mb-2 text-grey-700">
To verify ownership of the domain, please add the following TXT record and then click Add
Domain below.
@ -315,8 +312,8 @@
Cancel
</button>
</div>
</div>
<div v-else class="max-w-2xl w-full bg-white rounded-lg shadow-2xl p-6">
</template>
<template v-else v-slot:content>
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
@ -370,25 +367,22 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="domainDefaultRecipientModalOpen" @close="closeDomainDefaultRecipientModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Update Default Recipient
</h2>
<template v-slot:title> Update Default Recipient </template>
<template v-slot:content>
<p class="my-4 text-grey-700">
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.
</p>
<multiselect
v-model="defaultRecipient"
v-model="defaultRecipientId"
:options="recipientOptions"
:multiple="false"
mode="single"
value-prop="id"
:close-on-select="true"
:clear-on-select="false"
:searchable="false"
@ -396,8 +390,6 @@
placeholder="Select recipient"
label="email"
track-by="email"
:preselect-first="false"
:show-labels="false"
>
</multiselect>
<div class="mt-6">
@ -418,16 +410,12 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
<Modal :open="deleteDomainModalOpen" @close="closeDeleteModal">
<div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
<h2
class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
>
Delete domain
</h2>
<template v-slot:title> Delete domain </template>
<template v-slot:content>
<p class="mt-4 text-grey-700">
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.
@ -450,7 +438,7 @@
Cancel
</button>
</div>
</div>
</template>
</Modal>
</div>
</template>
@ -462,7 +450,7 @@ import { roundArrow } from 'tippy.js'
import 'tippy.js/dist/svg-arrow.css'
import 'tippy.js/dist/tippy.css'
import tippy from 'tippy.js'
import Multiselect from 'vue-multiselect'
import Multiselect from '@vueform/multiselect'
export default {
props: {
@ -507,7 +495,7 @@ export default {
checkRecordsLoading: false,
domainDefaultRecipientModalOpen: false,
defaultRecipientDomainToEdit: {},
defaultRecipient: {},
defaultRecipientId: null,
editDefaultRecipientLoading: false,
errors: {},
columns: [
@ -667,20 +655,20 @@ export default {
openDomainDefaultRecipientModal(domain) {
this.domainDefaultRecipientModalOpen = true
this.defaultRecipientDomainToEdit = domain
this.defaultRecipient = domain.default_recipient
this.defaultRecipientId = domain.default_recipient_id
},
closeDomainDefaultRecipientModal() {
this.domainDefaultRecipientModalOpen = false
this.defaultRecipientDomainToEdit = {}
this.defaultRecipient = {}
this.defaultRecipientId = null
},
openCheckRecordsModal(domain) {
this.domainToCheck = domain
this.addDomainModalOpen = true
},
closeCheckRecordsModal() {
this.domainToCheck = null
this.addDomainModalOpen = false
_.delay(() => (this.domainToCheck = null), 300)
},
editDomain(domain) {
if (this.domainDescriptionToEdit.length > 200) {
@ -716,7 +704,7 @@ export default {
.patch(
`/api/v1/domains/${this.defaultRecipientDomainToEdit.id}/default-recipient`,
JSON.stringify({
default_recipient: this.defaultRecipient ? this.defaultRecipient.id : '',
default_recipient: this.defaultRecipientId,
}),
{
headers: { 'Content-Type': 'application/json' },
@ -724,17 +712,18 @@ export default {
)
.then(response => {
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.editDefaultRecipientLoading = false
this.defaultRecipient = {}
this.defaultRecipientId = null
this.success("Domain's default recipient updated")
})
.catch(error => {
this.domainDefaultRecipientModalOpen = false
this.editDefaultRecipientLoading = false
this.defaultRecipient = {}
this.defaultRecipientId = null
this.error()
})
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,6 @@
<div class="container py-8">
@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>
@endsection

View file

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

View file

@ -4,6 +4,6 @@
<div class="container py-8">
@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>
@endsection

View file

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

View file

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\AliasExportController;
use App\Http\Controllers\Auth\ApiAuthenticationController;
use App\Http\Controllers\Auth\BackupCodeController;
use App\Http\Controllers\Auth\ForgotUsernameController;
use App\Http\Controllers\Auth\PersonalAccessTokenController;
@ -42,13 +43,16 @@ use Illuminate\Support\Facades\Route;
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::get('/username/reminder', 'show')->name('username.reminder.show');
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::get('/login/backup-code', 'index')->name('login.backup_code.index');

View file

@ -297,8 +297,6 @@ class AliasesTest extends TestCase
/** @test */
public function user_can_forget_alias()
{
$this->withoutExceptionHandling();
$alias = Alias::factory()->create([
'user_id' => $this->user->id
]);
@ -316,8 +314,6 @@ class AliasesTest extends TestCase
/** @test */
public function user_can_forget_shared_domain_alias()
{
$this->withoutExceptionHandling();
$sharedDomainAlias = Alias::factory()->create([
'user_id' => $this->user->id,
'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 */
public function user_can_receive_username_reminder_email()
{
$this->withoutMiddleware();
Notification::fake();
$recipient = $this->user->recipients[0];
@ -116,6 +118,8 @@ class LoginTest extends TestCase
/** @test */
public function username_reminder_email_not_sent_for_unkown_email()
{
$this->withoutMiddleware();
Notification::fake();
$this->post('/username/email', [

View file

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

View file

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