Compare commits

...

9 commits

Author SHA1 Message Date
Will Browning
fca7d88894 Updated packages 2024-12-09 16:45:54 +00:00
Will Browning
67c824ce1c Updated packages 2024-11-13 07:49:16 +00:00
Will Browning
abab703ecf Updated packages 2024-10-22 20:49:52 +01:00
Will Browning
e7045cc7f7 Updated API auth routes 2024-09-27 15:38:35 +01:00
Will Browning
c832b1b556 Updated packages 2024-09-04 15:51:29 +01:00
Will Browning
aa0addeacd Added Edge browser extension link 2024-08-28 09:44:50 +01:00
Will Browning
adddee0ba1 Rollback mews/catpcha to v3.3.3 2024-08-27 11:57:41 +01:00
Will Browning
5d0270d880 Fixed failing test 2024-08-23 14:21:50 +01:00
Will Browning
16933763d0 Added "last used at" sort option to aliases 2024-08-23 14:12:18 +01:00
32 changed files with 2008 additions and 1678 deletions

View file

@ -409,7 +409,7 @@ If you get close to your limit (over 80%) you'll be sent an email letting you kn
## Can I login using an additional username?
Yes, you can login with any of your usernames. You can add 1 additional username as a Lite user and up to 10 additional usernames as a Pro user for totals of 2 and 11 respectively (including the one you signed up with).
Yes, you can login with any of your usernames. You can add 5 additional username as a Lite user and up to 20 additional usernames as a Pro user for totals of 6 and 21 respectively (including the one you signed up with).
## I'm not receiving any emails, what's wrong?

View file

@ -260,7 +260,6 @@ smtpd_recipient_restrictions =
reject_rhsbl_reverse_client dbl.spamhaus.org,
reject_rhsbl_sender dbl.spamhaus.org,
reject_rbl_client zen.spamhaus.org
reject_rbl_client dul.dnsbl.sorbs.net
# Block clients that speak too early.
smtpd_data_restrictions = reject_unauth_pipelining
@ -1359,11 +1358,8 @@ npm run production
# Run any database migrations
php artisan migrate --force
# Clear cache
php artisan config:cache
php artisan view:cache
php artisan route:cache
php artisan event:cache
# Cache config, events, routes and views
php artisan optimize
# Restart queue workers to reflect changes
php artisan queue:restart

View file

@ -3,9 +3,10 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\GeneralAliasBulkRequest;
use App\Http\Requests\RecipientsAliasBulkRequest;
use App\Http\Resources\AliasResource;
use App\Rules\VerifiedRecipientId;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Ramsey\Uuid\Uuid;
@ -16,13 +17,8 @@ class AliasBulkController extends Controller
$this->middleware('throttle:12,1');
}
public function get(Request $request)
public function get(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliases = user()->aliases()->withTrashed()
->whereIn('id', $request->ids)
->get();
@ -35,13 +31,8 @@ class AliasBulkController extends Controller
return AliasResource::collection($aliases);
}
public function activate(Request $request)
public function activate(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasesWithTrashed = user()->aliases()->withTrashed()
->select(['id', 'user_id', 'active', 'deleted_at'])
->where('active', false)
@ -75,13 +66,8 @@ class AliasBulkController extends Controller
], 200);
}
public function deactivate(Request $request)
public function deactivate(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()
->where('active', true)
->whereIn('id', $request->ids)
@ -100,13 +86,8 @@ class AliasBulkController extends Controller
], 200);
}
public function delete(Request $request)
public function delete(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()
->whereIn('id', $request->ids)
->pluck('id');
@ -129,13 +110,8 @@ class AliasBulkController extends Controller
], 200);
}
public function forget(Request $request)
public function forget(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()->withTrashed()
->whereIn('id', $request->ids)
->pluck('id');
@ -183,13 +159,8 @@ class AliasBulkController extends Controller
], 200);
}
public function restore(Request $request)
public function restore(GeneralAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
]);
$aliasIds = user()->aliases()->onlyTrashed()
->whereIn('id', $request->ids)
->pluck('id');
@ -209,7 +180,7 @@ class AliasBulkController extends Controller
], 200);
}
public function recipients(Request $request)
public function recipients(RecipientsAliasBulkRequest $request)
{
$request->validate([
'ids' => 'required|array|max:25|min:1',

View file

@ -29,7 +29,30 @@ class AliasController extends Controller
->when($request->input('sort'), function ($query, $sort) {
$direction = strpos($sort, '-') === 0 ? 'desc' : 'asc';
$sort = ltrim($sort, '-');
$compareOperator = $direction === 'desc' ? '>' : '<';
// If sort is last_used then order by all and return
if ($sort === 'last_used') {
return $query
->orderByRaw(
"CASE
WHEN (last_forwarded {$compareOperator} last_replied
OR (last_forwarded IS NOT NULL
AND last_replied IS NULL))
AND (last_forwarded {$compareOperator} last_sent
OR (last_forwarded IS NOT NULL
AND last_sent IS NULL))
THEN last_forwarded
WHEN last_replied {$compareOperator} last_sent
OR (last_replied IS NOT NULL
AND last_sent IS NULL)
THEN last_replied
ELSE last_sent
END {$direction}"
)->orderBy('created_at', 'desc');
}
// If sort is created at then simply return as no need for secondary sorting below
if ($sort === 'created_at') {
return $query->orderBy($sort, $direction);
}

View file

@ -6,9 +6,12 @@ use App\Facades\Webauthn;
use App\Http\Controllers\Controller;
use App\Http\Requests\ApiAuthenticationLoginRequest;
use App\Http\Requests\ApiAuthenticationMfaRequest;
use App\Http\Requests\DestroyAccountRequest;
use App\Jobs\DeleteAccount;
use App\Models\User;
use App\Models\Username;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
@ -20,34 +23,50 @@ class ApiAuthenticationController extends Controller
public function __construct()
{
$this->middleware('throttle:3,1');
$this->middleware(['auth:sanctum', 'verified'])->only(['logout', 'destroy']);
}
public function login(ApiAuthenticationLoginRequest $request)
{
$user = Username::firstWhere('username', $request->username)?->user;
$user = Username::select(['user_id', 'username'])->firstWhere('username', $request->username)?->user;
if (! $user || ! Hash::check($request->password, $user->password)) {
return response()->json([
'error' => 'The provided credentials are incorrect',
'message' => 'The provided credentials are incorrect.',
], 401);
}
if (! $user->hasVerifiedDefaultRecipient()) {
return response()->json([
'message' => 'Your email address is not verified.',
], 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",
'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' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead',
'message' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead.',
], 403);
}
// day, week, month, year or null
if ($request->expiration) {
$method = 'add'.ucfirst($request->expiration);
$expiration = now()->{$method}();
} else {
$expiration = null;
}
// Token expires after 3 months, user must re-login
$newToken = $user->createToken($request->device_name, ['*'], now()->addMonths(3));
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
$token = $newToken->accessToken;
// If the user doesn't use 2FA then return the new API key
@ -65,7 +84,7 @@ class ApiAuthenticationController extends Controller
$mfaKey = Crypt::decryptString($request->mfa_key);
} catch (DecryptException $e) {
return response()->json([
'error' => 'Invalid mfa_key',
'message' => 'Invalid mfa_key.',
], 401);
}
$parts = explode('|', $mfaKey, 3);
@ -73,15 +92,17 @@ class ApiAuthenticationController extends Controller
$user = User::find($parts[0]);
if (! $user || $parts[1] !== config('anonaddy.secret')) {
return response()->json([
'error' => 'Invalid mfa_key',
'message' => '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',
'message' => 'mfa_key expired, please request a new one at /api/auth/login.',
], 401);
}
@ -91,8 +112,9 @@ class ApiAuthenticationController extends Controller
$timestamp = $google2fa->verifyKeyNewer($user->two_factor_secret, $request->otp, $lastTimeStamp, config('google2fa.window'));
if (! $timestamp) {
return response()->json([
'error' => 'The \'One Time Password\' typed was wrong',
'message' => 'The \'One Time Password\' typed was wrong.',
], 401);
}
@ -100,7 +122,15 @@ class ApiAuthenticationController extends Controller
Cache::put('2fa_ts:'.$user->id, $timestamp, now()->addMinutes(5));
}
$newToken = $user->createToken($request->device_name, ['*'], now()->addMonths(3));
// day, week, month, year or null
if ($request->expiration) {
$method = 'add'.ucfirst($request->expiration);
$expiration = now()->{$method}();
} else {
$expiration = null;
}
$newToken = $user->createToken($request->device_name, ['*'], $expiration);
$token = $newToken->accessToken;
return response()->json([
@ -110,4 +140,26 @@ class ApiAuthenticationController extends Controller
'expires_at' => $token->expires_at?->toDateTimeString(),
]);
}
public function logout(Request $request)
{
$token = $request->user()?->currentAccessToken();
if (! $token) {
return response()->json([
'message', 'API key not found.',
], 404);
}
$token->delete();
return response()->json([], 204);
}
public function destroy(DestroyAccountRequest $request)
{
DeleteAccount::dispatch($request->user());
return response()->json([], 204);
}
}

View file

@ -58,6 +58,7 @@ class ShowAliasController extends Controller
'last_blocked',
'last_replied',
'last_sent',
'last_used',
'active',
'created_at',
'updated_at',
@ -73,6 +74,7 @@ class ShowAliasController extends Controller
'-last_blocked',
'-last_replied',
'-last_sent',
'-last_used',
'-active',
'-created_at',
'-updated_at',
@ -95,10 +97,12 @@ class ShowAliasController extends Controller
$sort = $request->session()->get('aliasesSort', 'created_at');
$direction = $request->session()->get('aliasesSortDirection', 'desc');
$compareOperator = $request->session()->get('aliasesSortCompareOperator', '>');
if ($request->has('sort')) {
$direction = strpos($request->input('sort'), '-') === 0 ? 'desc' : 'asc';
$sort = ltrim($request->input('sort'), '-');
$compareOperator = $direction === 'desc' ? '>' : '<';
$request->session()->put('aliasesSort', $sort);
$request->session()->put('aliasesSortDirection', $direction);
@ -115,11 +119,32 @@ class ShowAliasController extends Controller
->when($request->input('username'), function ($query, $id) {
return $query->belongsToAliasable('App\Models\Username', $id);
})
->when($sort !== 'created_at' || $direction !== 'desc', function ($query) use ($sort, $direction) {
->when($sort !== 'created_at' || $direction !== 'desc', function ($query) use ($sort, $direction, $compareOperator) {
if ($sort === 'created_at') {
return $query->orderBy($sort, $direction);
}
// If sort is last_used then order by all and return
if ($sort === 'last_used') {
return $query
->orderByRaw(
"CASE
WHEN (last_forwarded {$compareOperator} last_replied
OR (last_forwarded IS NOT NULL
AND last_replied IS NULL))
AND (last_forwarded {$compareOperator} last_sent
OR (last_forwarded IS NOT NULL
AND last_sent IS NULL))
THEN last_forwarded
WHEN last_replied {$compareOperator} last_sent
OR (last_replied IS NOT NULL
AND last_sent IS NULL)
THEN last_replied
ELSE last_sent
END {$direction}"
)->orderBy('created_at', 'desc');
}
// Secondary order by latest first
return $query
->orderBy($sort, $direction)

View file

@ -24,9 +24,27 @@ class ApiAuthenticationLoginRequest extends FormRequest
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|string',
'device_name' => 'required|string|max:50',
'username' => [
'required',
'regex:/^[a-zA-Z0-9]*$/',
'min:1',
'max:20',
],
'password' => [
'required',
'string',
],
'device_name' => [
'required',
'string',
'max:50',
],
'expiration' => [
'nullable',
'string',
'max:5',
'in:day,week,month,year',
],
];
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
class GeneralAliasBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'ids' => Arr::whereNotNull($this->ids ?? []),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
];
}
}

View file

@ -75,6 +75,7 @@ class IndexAliasRequest extends FormRequest
'last_blocked',
'last_replied',
'last_sent',
'last_used',
'active',
'created_at',
'updated_at',
@ -90,6 +91,7 @@ class IndexAliasRequest extends FormRequest
'-last_blocked',
'-last_replied',
'-last_sent',
'-last_used',
'-active',
'-created_at',
'-updated_at',

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests;
use App\Rules\VerifiedRecipientId;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
class RecipientsAliasBulkRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'ids' => Arr::whereNotNull($this->ids ?? []),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'ids' => 'required|array|max:25|min:1',
'ids.*' => 'required|uuid|distinct',
'recipient_ids' => [
'array',
'max:10',
new VerifiedRecipientId,
],
'recipient_ids.*' => 'required|uuid|distinct',
];
}
}

View file

@ -30,7 +30,7 @@ class UserResource extends JsonResource
'banner_location' => $this->banner_location,
'bandwidth' => $this->bandwidth,
'bandwidth_limit' => $this->getBandwidthLimit(),
'username_count' => $this->username_count,
'username_count' => $this->usernames()->count(),
'username_limit' => config('anonaddy.additional_username_limit'),
'default_username_id' => $this->default_username_id,
'default_recipient_id' => $this->default_recipient_id,

View file

@ -41,7 +41,7 @@ class TokenExpiringSoon extends Mailable implements ShouldBeEncrypted, ShouldQue
public function build()
{
return $this
->subject('Your addy.io API key expires soon')
->subject('Your '.config('app.name').' API key expires soon')
->markdown('mail.token_expiring_soon', [
'user' => $this->user,
'userId' => $this->user->id,

View file

@ -430,6 +430,10 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasVerifiedDefaultRecipient()
{
if (! isset($this->defaultRecipient->email_verified_at)) {
return false;
}
return ! is_null($this->defaultRecipient->email_verified_at);
}
@ -499,7 +503,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function hasReachedUsernameLimit()
{
return $this->username_count >= config('anonaddy.additional_username_limit');
return $this->usernames()->count() >= config('anonaddy.additional_username_limit');
}
public function isVerifiedRecipient($email)

View file

@ -33,7 +33,7 @@ class UsernameReminder extends Notification implements ShouldBeEncrypted, Should
public function toMail($notifiable)
{
return (new MailMessage)
->subject('addy.io Username Reminder')
->subject(config('app.name').' Username Reminder')
->markdown('mail.username_reminder', [
'username' => $notifiable->user->username,
'userId' => $notifiable->user_id,

View file

@ -18,8 +18,8 @@
"laravel/tinker": "^2.7",
"laravel/ui": "^4.0",
"maatwebsite/excel": "^3.1",
"mews/captcha": "^3.0.0",
"php-mime-mail-parser/php-mime-mail-parser": "^8.0",
"mews/captcha": "3.3.3",
"php-mime-mail-parser/php-mime-mail-parser": "^9.0",
"pragmarx/google2fa-laravel": "^2.0.0",
"ramsey/uuid": "^4.0",
"tightenco/ziggy": "^2.0.0"

2009
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'addy.io'),
/*
|--------------------------------------------------------------------------

901
package-lock.json generated

File diff suppressed because it is too large Load diff

275
postfix/composer.lock generated
View file

@ -290,16 +290,16 @@
},
{
"name": "illuminate/collections",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/collections.git",
"reference": "2be24113fe25ef18be33bbd083ad36bf1e751eb5"
"reference": "fd2103ddc121449a7926fc34a9d220e5b88183c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/collections/zipball/2be24113fe25ef18be33bbd083ad36bf1e751eb5",
"reference": "2be24113fe25ef18be33bbd083ad36bf1e751eb5",
"url": "https://api.github.com/repos/illuminate/collections/zipball/fd2103ddc121449a7926fc34a9d220e5b88183c1",
"reference": "fd2103ddc121449a7926fc34a9d220e5b88183c1",
"shasum": ""
},
"require": {
@ -341,20 +341,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-07-23T14:08:10+00:00"
"time": "2024-11-27T14:51:56+00:00"
},
{
"name": "illuminate/conditionable",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/conditionable.git",
"reference": "362dd761b9920367bca1427a902158225e9e3a23"
"reference": "911df1bda950a3b799cf80671764e34eede131c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/362dd761b9920367bca1427a902158225e9e3a23",
"reference": "362dd761b9920367bca1427a902158225e9e3a23",
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/911df1bda950a3b799cf80671764e34eede131c6",
"reference": "911df1bda950a3b799cf80671764e34eede131c6",
"shasum": ""
},
"require": {
@ -387,20 +387,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-06-28T20:10:30+00:00"
"time": "2024-11-21T16:28:56+00:00"
},
{
"name": "illuminate/container",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "122c62229b209678013c0833793d7cf94d14bbbd"
"reference": "b057b0bbb38d7c7524df1ca5c38e7318f4c64d26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/122c62229b209678013c0833793d7cf94d14bbbd",
"reference": "122c62229b209678013c0833793d7cf94d14bbbd",
"url": "https://api.github.com/repos/illuminate/container/zipball/b057b0bbb38d7c7524df1ca5c38e7318f4c64d26",
"reference": "b057b0bbb38d7c7524df1ca5c38e7318f4c64d26",
"shasum": ""
},
"require": {
@ -438,20 +438,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-07-26T06:12:27+00:00"
"time": "2024-11-21T20:07:31+00:00"
},
{
"name": "illuminate/contracts",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "ebe2b8d69b8fb1c07111e3500d464e77dfab3202"
"reference": "184317f701ba20ca265e36808ed54b75b115972d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/ebe2b8d69b8fb1c07111e3500d464e77dfab3202",
"reference": "ebe2b8d69b8fb1c07111e3500d464e77dfab3202",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/184317f701ba20ca265e36808ed54b75b115972d",
"reference": "184317f701ba20ca265e36808ed54b75b115972d",
"shasum": ""
},
"require": {
@ -486,20 +486,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-07-29T06:48:51+00:00"
"time": "2024-11-25T15:33:38+00:00"
},
{
"name": "illuminate/database",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/database.git",
"reference": "db151e9a3221705cb4149c39dce2c51799708637"
"reference": "f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/database/zipball/db151e9a3221705cb4149c39dce2c51799708637",
"reference": "db151e9a3221705cb4149c39dce2c51799708637",
"url": "https://api.github.com/repos/illuminate/database/zipball/f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2",
"reference": "f08bb9ffa1d829cb6f8bb713e5c9cd3a47636ff2",
"shasum": ""
},
"require": {
@ -514,11 +514,12 @@
},
"suggest": {
"ext-filter": "Required to use the Postgres database driver.",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.21).",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.24).",
"illuminate/console": "Required to use the database commands (^11.0).",
"illuminate/events": "Required to use the observers with Eloquent (^11.0).",
"illuminate/filesystem": "Required to use the migrations (^11.0).",
"illuminate/pagination": "Required to paginate the result set (^11.0).",
"laravel/serializable-closure": "Required to handle circular references in model serialization (^1.3).",
"symfony/finder": "Required to use Eloquent model factories (^7.0)."
},
"type": "library",
@ -554,11 +555,11 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-07-30T07:00:12+00:00"
"time": "2024-11-25T22:44:52+00:00"
},
{
"name": "illuminate/macroable",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/macroable.git",
@ -604,16 +605,16 @@
},
{
"name": "illuminate/support",
"version": "v11.19.0",
"version": "v11.34.2",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "f3c19ee61d875dca1045df2d90000ae97f1645f9"
"reference": "2b718a86571baed50fdc5d5748a846c2e58e07eb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/f3c19ee61d875dca1045df2d90000ae97f1645f9",
"reference": "f3c19ee61d875dca1045df2d90000ae97f1645f9",
"url": "https://api.github.com/repos/illuminate/support/zipball/2b718a86571baed50fdc5d5748a846c2e58e07eb",
"reference": "2b718a86571baed50fdc5d5748a846c2e58e07eb",
"shasum": ""
},
"require": {
@ -625,9 +626,9 @@
"illuminate/conditionable": "^11.0",
"illuminate/contracts": "^11.0",
"illuminate/macroable": "^11.0",
"nesbot/carbon": "^2.72.2|^3.0",
"nesbot/carbon": "^2.72.2|^3.4",
"php": "^8.2",
"voku/portable-ascii": "^2.0"
"voku/portable-ascii": "^2.0.2"
},
"conflict": {
"tightenco/collect": "<5.5.33"
@ -637,12 +638,13 @@
},
"suggest": {
"illuminate/filesystem": "Required to use the composer class (^11.0).",
"laravel/serializable-closure": "Required to use the once function (^1.3).",
"league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).",
"ramsey/uuid": "Required to use Str::uuid() (^4.7).",
"symfony/process": "Required to use the composer class (^7.0).",
"symfony/uid": "Required to use Str::ulid() (^7.0).",
"symfony/var-dumper": "Required to use the dd function (^7.0).",
"vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)."
"vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)."
},
"type": "library",
"extra": {
@ -652,6 +654,7 @@
},
"autoload": {
"files": [
"functions.php",
"helpers.php"
],
"psr-4": {
@ -674,24 +677,24 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-07-30T06:57:25+00:00"
"time": "2024-11-27T14:58:17+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.7.0",
"version": "3.8.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139"
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cb4374784c87d0a0294e8513a52eb63c0aff3139",
"reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
"shasum": ""
},
"require": {
"carbonphp/carbon-doctrine-types": "*",
"carbonphp/carbon-doctrine-types": "<100.0",
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
@ -780,7 +783,7 @@
"type": "tidelift"
}
],
"time": "2024-07-16T22:29:20+00:00"
"time": "2024-11-07T17:46:48+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@ -1078,16 +1081,16 @@
},
{
"name": "symfony/clock",
"version": "v7.1.1",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
"reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7"
"reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7",
"reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7",
"url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
"reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24",
"shasum": ""
},
"require": {
@ -1132,7 +1135,7 @@
"time"
],
"support": {
"source": "https://github.com/symfony/clock/tree/v7.1.1"
"source": "https://github.com/symfony/clock/tree/v7.2.0"
},
"funding": [
{
@ -1148,24 +1151,91 @@
"type": "tidelift"
}
],
"time": "2024-05-31T14:57:53+00:00"
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.30.0",
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@ -1176,8 +1246,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1211,7 +1281,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@ -1227,24 +1297,24 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@ -1291,7 +1361,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@ -1307,30 +1377,30 @@
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1371,7 +1441,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@ -1387,30 +1457,30 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9"
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1447,7 +1517,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
},
"funding": [
{
@ -1463,24 +1533,25 @@
"type": "tidelift"
}
],
"time": "2024-06-19T12:35:24+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/translation",
"version": "v7.1.3",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1"
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/8d5e50c813ba2859a6dfc99a0765c550507934a1",
"reference": "8d5e50c813ba2859a6dfc99a0765c550507934a1",
"url": "https://api.github.com/repos/symfony/translation/zipball/dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"reference": "dc89e16b44048ceecc879054e5b7f38326ab6cc5",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0"
},
@ -1541,7 +1612,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.1.3"
"source": "https://github.com/symfony/translation/tree/v7.2.0"
},
"funding": [
{
@ -1557,20 +1628,20 @@
"type": "tidelift"
}
],
"time": "2024-07-26T12:41:01+00:00"
"time": "2024-11-12T20:47:56+00:00"
},
{
"name": "symfony/translation-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
"shasum": ""
},
"require": {
@ -1619,7 +1690,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
},
"funding": [
{
@ -1635,7 +1706,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "vlucas/phpdotenv",
@ -1723,16 +1794,16 @@
},
{
"name": "voku/portable-ascii",
"version": "2.0.1",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/voku/portable-ascii.git",
"reference": "b56450eed252f6801410d810c8e1727224ae0743"
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743",
"reference": "b56450eed252f6801410d810c8e1727224ae0743",
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
"reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d",
"shasum": ""
},
"require": {
@ -1757,7 +1828,7 @@
"authors": [
{
"name": "Lars Moelleken",
"homepage": "http://www.moelleken.org/"
"homepage": "https://www.moelleken.org/"
}
],
"description": "Portable ASCII library - performance optimized (ascii) string functions for php.",
@ -1769,7 +1840,7 @@
],
"support": {
"issues": "https://github.com/voku/portable-ascii/issues",
"source": "https://github.com/voku/portable-ascii/tree/2.0.1"
"source": "https://github.com/voku/portable-ascii/tree/2.0.3"
},
"funding": [
{
@ -1793,7 +1864,7 @@
"type": "tidelift"
}
],
"time": "2022-03-08T17:03:00+00:00"
"time": "2024-11-21T01:49:47+00:00"
}
],
"packages-dev": [],

View file

@ -1162,7 +1162,7 @@
</p>
<p class="mt-4 text-grey-700">
To send from an alias you must send the email from a <b>verified recipient</b> on your
addy.io account.
account.
</p>
<label for="send_from_alias" class="block font-medium leading-6 text-grey-600 text-sm my-2">
Alias to send from
@ -1575,6 +1575,10 @@ const sortOptions = [
value: 'last_sent',
label: 'Last Sent At',
},
{
value: 'last_used',
label: 'Last Used At',
},
{
value: 'updated_at',
label: 'Updated At',

View file

@ -1003,7 +1003,7 @@ const validateNewDomain = e => {
if (!newDomain.value) {
errors.value.newDomain = 'Domain name required'
} else if (newDomain.value.length > 50) {
} else if (newDomain.value.length > 100) {
errors.value.newDomain = 'That domain name is too long'
} else if (!validDomain(newDomain.value)) {
errors.value.newDomain = 'Please enter a valid domain name'

View file

@ -21,13 +21,21 @@
class="text-indigo-700"
>Firefox</a
>
or
,
<a
href="https://chrome.google.com/webstore/detail/addyio-anonymous-email-fo/iadbdpnoknmbdeolbapdackdcogdmjpe"
target="_blank"
rel="nofollow noopener noreferrer"
class="text-indigo-700"
>Chrome / Brave</a
>Chrome, Brave</a
>
or
<a
href="https://microsoftedge.microsoft.com/addons/detail/addyio-anonymous-email/ohjlgpcfncgkijjfmabldlgnccmgcehl"
target="_blank"
rel="nofollow noopener noreferrer"
class="text-indigo-700"
>Edge</a
>
to create new aliases. They can also be used with the mobile apps. Simply paste a key
you've created into the browser extension or mobile apps to get started. Your API access
@ -197,7 +205,7 @@
<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 addy.io mobile app by Stjin.
You can scan this QR code to automatically login to the addy.io mobile app.
</p>
</div>
<div class="mt-6">

View file

@ -721,7 +721,7 @@
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
<button
@click="disableKey"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disable:cursor-not-allowed"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
:disabled="disableKeyLoading"
>
Disable
@ -771,7 +771,7 @@
<div class="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
<button
@click="deleteKey"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disable:cursor-not-allowed"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
:disabled="deleteKeyLoading"
>
Remove

View file

@ -2,7 +2,7 @@
# Domain MX records invalid
A recent DNS record check on your custom domain **{{ $domain }}** on addy.io showed that your MX records are no longer pointing to the addy.io server. This means that addy.io will not be able to handle your emails for you.
A recent DNS record check on your custom domain **{{ $domain }}** on {{ config('app.name') }} showed that your MX records are no longer pointing to the {{ config('app.name') }} server. This means that {{ config('app.name') }} will not be able to handle your emails for you.
If this MX record change was intentional then you can ignore this email.

View file

@ -2,7 +2,7 @@
# Domain Unverified For Sending
A recent DNS record check on your custom domain **{{ $domain }}** failed on addy.io. This means that your domain had been unverified for sending until the DNS records are added correctly.
A recent DNS record check on your custom domain **{{ $domain }}** failed on {{ config('app.name') }}. This means that your domain had been unverified for sending until the DNS records are added correctly.
The check failed for the following reason:
@ -10,7 +10,7 @@ The check failed for the following reason:
Please visit the domains page on the site by clicking the button below to resolve the issue.
Emails for your custom domain will be sent from an addy.io domain in the mean time.
Emails for your custom domain will be sent from an {{ config('app.name') }} domain in the mean time.
@component('mail::button', ['url' => config('app.url').'/domains'])
Check Domain

View file

@ -2,7 +2,7 @@
# Failed two factor authentication login attempt
Someone just entered an incorrect OTP while trying to login to your addy.io account. The username (**{{ $username }}**) and password were correct.
Someone just entered an incorrect OTP while trying to login to your {{ config('app.name') }} account. The username (**{{ $username }}**) and password were correct.
The login has been blocked. If this was you, then you can ignore this notification.

View file

@ -3,9 +3,9 @@
# Your API key expires soon
@if($tokenName)
Your API key named "**{{ $tokenName }}**" on your addy.io account expires in **one weeks time**.
Your API key named "**{{ $tokenName }}**" on your {{ config('app.name') }} account expires in **one weeks time**.
@else
One of the API keys on your addy.io account will expire in **one weeks time**.
One of the API keys on your {{ config('app.name') }} account will expire in **one weeks time**.
@endif
If you are not using this API key for the browser extensions, mobile apps or to access the API then you do not need to take any action.

View file

@ -28,6 +28,7 @@ use App\Http\Controllers\Api\ReorderRuleController;
use App\Http\Controllers\Api\RuleController;
use App\Http\Controllers\Api\UsernameController;
use App\Http\Controllers\Api\UsernameDefaultRecipientController;
use App\Http\Controllers\Auth\ApiAuthenticationController;
use App\Http\Controllers\RecipientVerificationController;
use Illuminate\Support\Facades\Route;
@ -42,6 +43,12 @@ use Illuminate\Support\Facades\Route;
|
*/
// API auth routes for mobile apps and browser extension
Route::controller(ApiAuthenticationController::class)->prefix('auth')->group(function () {
Route::post('/logout', 'logout');
Route::post('/delete-account', 'destroy');
});
Route::group([
'middleware' => ['auth:sanctum', 'verified'],
'prefix' => 'v1',

View file

@ -51,9 +51,11 @@ 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']);
// API login route needs CSRF middleware so that it can pass it to api/auth/mfa
Route::controller(ApiAuthenticationController::class)->prefix('api/auth')->group(function () {
Route::post('/login', 'login');
Route::post('/mfa', 'mfa');
});
Route::controller(ForgotUsernameController::class)->group(function () {
Route::get('/username/reminder', 'show')->name('username.reminder.show');

View file

@ -441,6 +441,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -482,6 +483,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -544,6 +546,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -575,6 +578,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -607,6 +611,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -637,6 +642,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
]);
@ -669,6 +675,7 @@ class AliasesTest extends TestCase
'ids' => [
$alias->id,
$alias2->id,
null,
],
'recipient_ids' => [
$recipient->id,
@ -683,4 +690,41 @@ class AliasesTest extends TestCase
'recipient_id' => $recipient->id,
]);
}
#[Test]
public function user_cannot_bulk_update_recipients_for_invalid_aliases()
{
$alias = Alias::factory()->create([
'user_id' => $this->user->id,
'active' => true,
]);
$alias2 = Alias::factory()->create([
'user_id' => '00000000-0000-0000-0000-000000000000',
'active' => true,
]);
$recipient = Recipient::factory()->create([
'user_id' => $this->user->id,
]);
$response = $this->json('POST', '/api/v1/aliases/recipients/bulk', [
'ids' => [
$alias->id,
$alias2->id,
null,
],
'recipient_ids' => [
$recipient->id,
],
]);
$response->assertStatus(200);
$this->assertCount(1, $response->getData()->ids);
$this->assertEquals('recipients updated for 1 alias successfully', $response->getData()->message);
$this->assertDatabaseHas('alias_recipients', [
'alias_id' => $alias->id,
'recipient_id' => $recipient->id,
]);
}
}

View file

@ -88,8 +88,8 @@ class UsernamesTest extends TestCase
]);
$response->assertStatus(403);
$this->assertEquals(3, $this->user->username_count);
$this->assertCount(4, $this->user->usernames);
$this->assertEquals(2, $this->user->username_count);
$this->assertCount(3, $this->user->usernames);
}
#[Test]

View file

@ -62,7 +62,7 @@ class ApiAuthenticationTest extends TestCase
]);
$response->assertUnauthorized();
$response->assertExactJson(['error' => 'The provided credentials are incorrect']);
$response->assertExactJson(['message' => 'The provided credentials are incorrect.']);
}
#[Test]
@ -120,7 +120,7 @@ class ApiAuthenticationTest extends TestCase
]);
$response->assertForbidden();
$response->assertExactJson(['error' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead']);
$response->assertExactJson(['message' => 'Security key authentication is not currently supported from the extension or mobile apps, please use an API key to login instead.']);
}
#[Test]
@ -156,7 +156,7 @@ class ApiAuthenticationTest extends TestCase
]);
$response2->assertUnauthorized();
$response2->assertExactJson(['error' => 'The \'One Time Password\' typed was wrong']);
$response2->assertExactJson(['message' => 'The \'One Time Password\' typed was wrong.']);
$response3 = $this->withHeaders([
'X-CSRF-TOKEN' => $csrfToken,
@ -169,4 +169,88 @@ class ApiAuthenticationTest extends TestCase
$response3->assertSuccessful();
$this->assertEquals($this->user->tokens[0]->token, hash('sha256', $response3->json()['api_key']));
}
#[Test]
public function user_can_logout_via_api()
{
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
$this->user->defaultUsername->username = 'janedoe';
$this->user->defaultUsername->save();
$token = $this->user->createToken('New');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
])->json('POST', '/api/auth/logout', []);
$response->assertStatus(204);
$this->assertDatabaseMissing('personal_access_tokens', [
'tokenable_id' => $this->user->id,
]);
}
#[Test]
public function user_can_delete_account_via_api()
{
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
$this->user->defaultUsername->username = 'janedoe';
$this->user->defaultUsername->save();
$token = $this->user->createToken('New');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
])->json('POST', '/api/auth/delete-account', [
'password' => 'mypassword',
]);
$response->assertStatus(204);
$this->assertDatabaseMissing('usernames', [
'username' => 'janedoe',
]);
}
#[Test]
public function user_must_enter_correct_password_to_delete_account()
{
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
$this->user->defaultUsername->username = 'janedoe';
$this->user->defaultUsername->save();
$token = $this->user->createToken('New');
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$token->plainTextToken,
])->json('POST', '/api/auth/delete-account', [
'password' => 'incorrect',
]);
$response->assertJsonValidationErrorFor('password');
$this->assertDatabaseHas('usernames', [
'username' => 'janedoe',
]);
}
#[Test]
public function user_must_have_valid_api_key_to_delete_account()
{
$this->withoutMiddleware(ThrottleRequestsWithRedis::class);
$this->user->defaultUsername->username = 'janedoe';
$this->user->defaultUsername->save();
$response = $this->withHeaders([
'Authorization' => 'Bearer invalid-api-key',
])->json('POST', '/api/auth/delete-account', [
'password' => 'mypassword',
]);
$response->assertUnauthorized();
$this->assertDatabaseHas('usernames', [
'username' => 'janedoe',
]);
}
}