diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6fd79..2c3a0dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ v1.7 - Fix FLEXIAPI-284 Add configurable admin API Keys - Fix FLEXIAPI-232 Add provisioning email + important redesign of the contacts page - Fix FLEXIAPI-287 Refactor the emails templates +- Fix FLEXIAPI-286 Send an account_recovery_token using a push notification and protect the account recovery using phone page with the account_recovery_token v1.6 ---- diff --git a/RELEASE.md b/RELEASE.md index d724f79..631facf 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,11 +11,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - **Spaces:** A new way to manage your SIP domains and hosts. A Space is defined by a unique SIP Domain and Host pair. - **New mandatory DotEnv variable** `APP_ROOT_HOST`, replaces `APP_URL` and `APP_SIP_DOMAIN` that are now configured using the new dedicated Artisan script. It defines the root hostname where all the Spaces will be configured. All the Spaces will be as subdomains of `APP_ROOT_HOST` except one that can be equal to `APP_ROOT_HOST`. Example: if `APP_ROOT_HOST=myhost.com` the Spaces hosts will be `myhost.com`, `alpha.myhost.com` , `beta.myhost.com`... + - **New DotEnv variable:** `APP_ACCOUNT_RECOVERY_TOKEN_EXPIRATION_MINUTES=0` Number of minutes before expiring the recovery tokens - **New Artisan script** `php artisan spaces:create-update {sip_domain} {host} {name} {--super}`, replaces `php artisan sip_domains:create-update {sip_domain} {--super}`. Can create a Space or update a Space Host base on its Space SIP Domain. ### Changed -- **Removing and moving DotEnv instance environnement variables to the Spaces** The following DotEnv variables were removed. You can now configure them directly in the designated spaces. +- **Removing and moving DotEnv instance environnement variables to the Spaces** The following DotEnv variables were removed. You can now configure them directly in the designated spaces after the migration. - INSTANCE_COPYRIGHT - INSTANCE_INTRO_REGISTRATION - INSTANCE_CUSTOM_THEME diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 55b08ed..d2426b4 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -21,6 +21,7 @@ APP_DANGEROUS_ENDPOINTS=false # Enable some dangerous endpoints used for XMLRPC # Expiration time for tokens and code, in minutes, 0 means no expiration APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two consecutive account_creation_token creation APP_ACCOUNT_CREATION_TOKEN_EXPIRATION_MINUTES=0 +APP_ACCOUNT_RECOVERY_TOKEN_EXPIRATION_MINUTES=0 APP_EMAIL_CHANGE_CODE_EXPIRATION_MINUTES=10 APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES=10 APP_RECOVERY_CODE_EXPIRATION_MINUTES=10 diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 67b41a2..80e3d8b 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -256,6 +256,11 @@ class Account extends Authenticatable return $this->hasOne(AccountCreationToken::class); } + public function accountRecoveryTokens() + { + return $this->hasMany(AccountRecoveryToken::class); + } + public function authTokens() { return $this->hasMany(AuthToken::class); @@ -385,10 +390,12 @@ class Account extends Authenticatable return $authToken; } - public function recover(?string $code = null): string + public function recover(?string $code = null, ?string $phone = null, ?string $email = null): string { $recoveryCode = new RecoveryCode; $recoveryCode->code = $code ?? generatePin(); + $recoveryCode->phone = $phone; + $recoveryCode->email = $email; $recoveryCode->account_id = $this->id; if (request()) { diff --git a/flexiapi/app/AccountRecoveryToken.php b/flexiapi/app/AccountRecoveryToken.php new file mode 100644 index 0000000..15eea05 --- /dev/null +++ b/flexiapi/app/AccountRecoveryToken.php @@ -0,0 +1,42 @@ +belongsTo(Account::class); + } + + public function consume() + { + $this->used = true; + $this->save(); + } + + public function consumed(): bool + { + return $this->used == true; + } + + public function toLog() + { + return [ + 'token' => $this->token, + 'pn_param' => $this->pn_param, + 'used' => $this->used, + 'account_id' => $this->account_id, + 'ip' => $this->ip, + 'user_agent' => $this->user_agent, + ]; + } +} diff --git a/flexiapi/app/Http/Controllers/Account/RecoveryController.php b/flexiapi/app/Http/Controllers/Account/RecoveryController.php index 35a0aa3..6c749ea 100644 --- a/flexiapi/app/Http/Controllers/Account/RecoveryController.php +++ b/flexiapi/app/Http/Controllers/Account/RecoveryController.php @@ -20,12 +20,16 @@ namespace App\Http\Controllers\Account; use App\Account; +use App\AccountRecoveryToken; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Services\AccountService; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Str; +use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; + class RecoveryController extends Controller { public function showEmail(Request $request) @@ -36,10 +40,16 @@ class RecoveryController extends Controller ]); } - public function showPhone(Request $request) + public function showPhone(Request $request, string $accountRecoveryToken) { + $accountRecoveryToken = AccountRecoveryToken::where('token', $accountRecoveryToken) + ->where('used', false) + ->firstOrFail(); + return view('account.recovery.show', [ 'method' => 'phone', + 'account_recovery_token' => $accountRecoveryToken->token, + 'phone' => $request->get('phone'), 'domain' => resolveDomain($request) ]); } @@ -49,7 +59,8 @@ class RecoveryController extends Controller $rules = [ 'email' => 'required_without:phone|email|exists:accounts,email', 'phone' => 'required_without:email|starts_with:+', - 'h-captcha-response' => captchaConfigured() ? 'required|HCaptcha' : '', + 'h-captcha-response' => captchaConfigured() ? 'required_if:email|HCaptcha' : '', + 'account_recovery_token' => 'required_if:phone', ]; $account = null; @@ -94,9 +105,17 @@ class RecoveryController extends Controller } if ($request->get('email')) { - $account = (new AccountService)->recoverByEmail($account); + $account = (new AccountService)->recoverByEmail($account, $request->get('email')); } elseif ($request->get('phone')) { - $account = (new AccountService)->recoverByPhone($account); + $accountRecoveryToken = AccountRecoveryToken::where('token', $request->get('account_recovery_token')) + ->where('used', false) + ->first(); + + if (!$accountRecoveryToken) { + abort(403, 'Wrong Account Recovery Token'); + } + + $account = (new AccountService)->recoverByPhone($account, $request->get('phone'), $accountRecoveryToken); } return view('account.recovery.confirm', [ diff --git a/flexiapi/app/Http/Controllers/Admin/ProvisioningEmailController.php b/flexiapi/app/Http/Controllers/Admin/ProvisioningEmailController.php index f7b2aa9..036a7a3 100644 --- a/flexiapi/app/Http/Controllers/Admin/ProvisioningEmailController.php +++ b/flexiapi/app/Http/Controllers/Admin/ProvisioningEmailController.php @@ -22,7 +22,7 @@ class ProvisioningEmailController extends Controller ]); } - public function send(Request $request, int $accountId) + public function send(int $accountId) { $account = Account::findOrFail($accountId); $account->provision(); diff --git a/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php b/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php index 93bfa0d..044836a 100644 --- a/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php +++ b/flexiapi/app/Http/Controllers/Api/Account/CreationTokenController.php @@ -66,7 +66,7 @@ class CreationTokenController extends Controller $fp = new FlexisipPusherConnector($token->pn_provider, $token->pn_param, $token->pn_prid); if ($fp->sendToken($token->token)) { - Log::channel('events')->info('API: Token sent', ['token' => $token->token]); + Log::channel('events')->info('API: Account Creation Token sent', ['token' => $token->token]); $token->save(); return; diff --git a/flexiapi/app/Http/Controllers/Api/Account/RecoveryTokenController.php b/flexiapi/app/Http/Controllers/Api/Account/RecoveryTokenController.php new file mode 100644 index 0000000..bf153d0 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Account/RecoveryTokenController.php @@ -0,0 +1,75 @@ +. +*/ + +namespace App\Http\Controllers\Api\Account; + +use Illuminate\Http\Request; +use Illuminate\Support\Str; +use Illuminate\Support\Facades\Log; +use Carbon\Carbon; + +use App\AccountRecoveryToken; +use App\Rules\PnParam; +use App\Rules\PnPrid; +use App\Rules\PnProvider; +use App\Http\Controllers\Controller; +use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; +use App\Libraries\FlexisipPusherConnector; + +class RecoveryTokenController extends Controller +{ + public function sendByPush(Request $request) + { + $request->validate([ + 'pn_provider' => ['required', new PnProvider], + 'pn_param' => [new PnParam], + 'pn_prid' => [new PnPrid], + ]); + + $last = AccountRecoveryToken::where('pn_provider', $request->get('pn_provider')) + ->where('pn_param', $request->get('pn_param')) + ->where('pn_prid', $request->get('pn_prid')) + ->where('created_at', '>=', Carbon::now()->subMinutes(config('app.account_recovery_token_retry_minutes'))->toDateTimeString()) + ->where('used', true) + ->latest() + ->first(); + + if ($last) { + Log::channel('events')->info('API: Token throttled', ['token' => $last->token]); + abort(429, 'Last token requested too recently'); + } + + $token = new AccountRecoveryToken; + $token->token = Str::random(WebAuthenticateController::$emailCodeSize); + $token->pn_provider = $request->get('pn_provider'); + $token->pn_param = $request->get('pn_param'); + $token->pn_prid = $request->get('pn_prid'); + $token->fillRequestInfo($request); + + $fp = new FlexisipPusherConnector($token->pn_provider, $token->pn_param, $token->pn_prid); + if ($fp->sendToken($token->token)) { + Log::channel('events')->info('API: AccountRecoveryToken sent', ['token' => $token->token]); + + $token->save(); + return; + } + + abort(503, "Token not sent"); + } +} diff --git a/flexiapi/app/Services/AccountService.php b/flexiapi/app/Services/AccountService.php index 86928f2..56d7787 100644 --- a/flexiapi/app/Services/AccountService.php +++ b/flexiapi/app/Services/AccountService.php @@ -21,6 +21,7 @@ namespace App\Services; use App\Account; use App\AccountCreationToken; +use App\AccountRecoveryToken; use App\EmailChangeCode; use App\Http\Requests\Account\Create\Request as CreateRequest; use App\Http\Requests\Account\Update\Request as UpdateRequest; @@ -220,7 +221,7 @@ class AccountService $account = $request->user(); - $phoneChangeCode = $account->phoneChangeCode ?? new PhoneChangeCode(); + $phoneChangeCode = new PhoneChangeCode(); $phoneChangeCode->account_id = $account->id; $phoneChangeCode->phone = $request->get('phone'); $phoneChangeCode->code = generatePin(); @@ -255,7 +256,7 @@ class AccountService $account = $request->user(); - $phoneChangeCode = $account->phoneChangeCode()->firstOrFail(); + $phoneChangeCode = $account->phoneChangeCodes()->firstOrFail(); if ($phoneChangeCode->expired()) { return abort(410, 'Expired code'); @@ -299,7 +300,7 @@ class AccountService $account = $request->user(); - $emailChangeCode = $account->emailChangeCode ?? new EmailChangeCode(); + $emailChangeCode = new EmailChangeCode(); $emailChangeCode->account_id = $account->id; $emailChangeCode->email = $request->get('email'); $emailChangeCode->code = generatePin(); @@ -327,7 +328,7 @@ class AccountService $account = $request->user(); - $emailChangeCode = $account->emailChangeCode()->firstOrFail(); + $emailChangeCode = $account->emailChangeCodes()->firstOrFail(); if ($emailChangeCode->expired()) { return abort(410, 'Expired code'); @@ -360,9 +361,11 @@ class AccountService * Account recovery */ - public function recoverByEmail(Account $account): Account + public function recoverByEmail(Account $account, string $email): Account { - $account = $this->recoverAccount($account); + $account->recover(email: $email); + $account->provision(); + $account->refresh(); Mail::to($account)->send(new RecoverByCode($account)); @@ -371,9 +374,11 @@ class AccountService return $account; } - public function recoverByPhone(Account $account): Account + public function recoverByPhone(Account $account, string $phone, AccountRecoveryToken $accountRecoveryToken): Account { - $account = $this->recoverAccount($account); + $account->recover(phone: $phone); + $account->provision(); + $account->refresh(); $message = 'Your ' . $account->space->name . ' validation code is ' . $account->recovery_code . '.'; @@ -386,14 +391,11 @@ class AccountService Log::channel('events')->info('Account Service: Sending recovery SMS', ['id' => $account->identifier]); - return $account; - } + $accountRecoveryToken->consume(); + $accountRecoveryToken->account_id = $account->id; + $accountRecoveryToken->save(); - private function recoverAccount(Account $account): Account - { - $account->recover(); - $account->provision(); - $account->refresh(); + Log::channel('events')->info('API: AccountRecoveryToken redeemed', ['account_recovery_token' => $accountRecoveryToken->toLog()]); return $account; } diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index f81e64f..20163d0 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -1324,16 +1324,16 @@ }, { "name": "giggsey/libphonenumber-for-php-lite", - "version": "9.0.3", + "version": "9.0.4", "source": { "type": "git", "url": "https://github.com/giggsey/libphonenumber-for-php-lite.git", - "reference": "e4f4c5834d35773bf9d5e72e3e764cfe4e301a8c" + "reference": "6b36e32fddce37738c4f6df66e49dd9a2475841c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/e4f4c5834d35773bf9d5e72e3e764cfe4e301a8c", - "reference": "e4f4c5834d35773bf9d5e72e3e764cfe4e301a8c", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/6b36e32fddce37738c4f6df66e49dd9a2475841c", + "reference": "6b36e32fddce37738c4f6df66e49dd9a2475841c", "shasum": "" }, "require": { @@ -1398,7 +1398,7 @@ "issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues", "source": "https://github.com/giggsey/libphonenumber-for-php-lite" }, - "time": "2025-04-11T07:36:14+00:00" + "time": "2025-04-28T07:26:55+00:00" }, { "name": "graham-campbell/result-type", @@ -2405,16 +2405,16 @@ }, { "name": "league/commonmark", - "version": "2.6.2", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "06c3b0bf2540338094575612f4a1778d0d2d5e94" + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/06c3b0bf2540338094575612f4a1778d0d2d5e94", - "reference": "06c3b0bf2540338094575612f4a1778d0d2d5e94", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "shasum": "" }, "require": { @@ -2451,7 +2451,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -2508,7 +2508,7 @@ "type": "tidelift" } ], - "time": "2025-04-18T21:09:27+00:00" + "time": "2025-05-05T12:20:28+00:00" }, { "name": "league/config", @@ -2950,16 +2950,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -2998,7 +2998,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -3006,7 +3006,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "namoshek/laravel-redis-sentinel", @@ -4096,16 +4096,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.45", + "version": "10.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", - "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d", + "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d", "shasum": "" }, "require": { @@ -4115,7 +4115,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -4177,7 +4177,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46" }, "funding": [ { @@ -4188,12 +4188,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-02-06T16:08:12+00:00" + "time": "2025-05-02T06:46:24+00:00" }, { "name": "propaganistas/laravel-phone", @@ -6818,16 +6826,16 @@ }, { "name": "symfony/console", - "version": "v6.4.20", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" + "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", - "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", + "url": "https://api.github.com/repos/symfony/console/zipball/a3011c7b7adb58d89f6c0d822abb641d7a5f9719", + "reference": "a3011c7b7adb58d89f6c0d822abb641d7a5f9719", "shasum": "" }, "require": { @@ -6892,7 +6900,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.20" + "source": "https://github.com/symfony/console/tree/v6.4.21" }, "funding": [ { @@ -6908,7 +6916,7 @@ "type": "tidelift" } ], - "time": "2025-03-03T17:16:38+00:00" + "time": "2025-04-07T15:42:41+00:00" }, { "name": "symfony/css-selector", @@ -7339,16 +7347,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.18", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db" + "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0492d6217e5ab48f51fca76f64cf8e78919d0db", - "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f0c7ea41db479383b81d436b836d37168fd5b99", + "reference": "3f0c7ea41db479383b81d436b836d37168fd5b99", "shasum": "" }, "require": { @@ -7396,7 +7404,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.18" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.21" }, "funding": [ { @@ -7412,20 +7420,20 @@ "type": "tidelift" } ], - "time": "2025-01-09T15:48:56+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.20", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6be6db31bc74693ce5516e1fd5e5ff1171005e37" + "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6be6db31bc74693ce5516e1fd5e5ff1171005e37", - "reference": "6be6db31bc74693ce5516e1fd5e5ff1171005e37", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/983ca05eec6623920d24ec0f1005f487d3734a0c", + "reference": "983ca05eec6623920d24ec0f1005f487d3734a0c", "shasum": "" }, "require": { @@ -7510,7 +7518,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.20" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.21" }, "funding": [ { @@ -7526,20 +7534,20 @@ "type": "tidelift" } ], - "time": "2025-03-28T13:27:10+00:00" + "time": "2025-05-02T08:46:38+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.18", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "e93a6ae2767d7f7578c2b7961d9d8e27580b2b11" + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e93a6ae2767d7f7578c2b7961d9d8e27580b2b11", - "reference": "e93a6ae2767d7f7578c2b7961d9d8e27580b2b11", + "url": "https://api.github.com/repos/symfony/mailer/zipball/ada2809ccd4ec27aba9fc344e3efdaec624c6438", + "reference": "ada2809ccd4ec27aba9fc344e3efdaec624c6438", "shasum": "" }, "require": { @@ -7590,7 +7598,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.18" + "source": "https://github.com/symfony/mailer/tree/v6.4.21" }, "funding": [ { @@ -7606,20 +7614,20 @@ "type": "tidelift" } ], - "time": "2025-01-24T15:27:15+00:00" + "time": "2025-04-26T23:47:35+00:00" }, { "name": "symfony/mime", - "version": "v6.4.19", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3" + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3", - "reference": "ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3", + "url": "https://api.github.com/repos/symfony/mime/zipball/fec8aa5231f3904754955fad33c2db50594d22d1", + "reference": "fec8aa5231f3904754955fad33c2db50594d22d1", "shasum": "" }, "require": { @@ -7675,7 +7683,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.19" + "source": "https://github.com/symfony/mime/tree/v6.4.21" }, "funding": [ { @@ -7691,11 +7699,11 @@ "type": "tidelift" } ], - "time": "2025-02-17T21:23:52+00:00" + "time": "2025-04-27T13:27:38+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -7754,7 +7762,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -7774,7 +7782,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -7832,7 +7840,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -7852,16 +7860,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -7915,7 +7923,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { @@ -7931,11 +7939,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -7996,7 +8004,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -8016,19 +8024,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -8076,7 +8085,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -8092,20 +8101,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -8156,7 +8165,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -8172,11 +8181,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -8232,7 +8241,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -8252,7 +8261,7 @@ }, { "name": "symfony/polyfill-uuid", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -8311,7 +8320,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" }, "funding": [ { @@ -8558,16 +8567,16 @@ }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", + "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", "shasum": "" }, "require": { @@ -8624,7 +8633,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v6.4.21" }, "funding": [ { @@ -8640,20 +8649,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:12+00:00" + "time": "2025-04-18T15:23:29+00:00" }, { "name": "symfony/translation", - "version": "v6.4.19", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "3b9bf9f33997c064885a7bfc126c14b9daa0e00e" + "reference": "bb92ea5588396b319ba43283a5a3087a034cb29c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/3b9bf9f33997c064885a7bfc126c14b9daa0e00e", - "reference": "3b9bf9f33997c064885a7bfc126c14b9daa0e00e", + "url": "https://api.github.com/repos/symfony/translation/zipball/bb92ea5588396b319ba43283a5a3087a034cb29c", + "reference": "bb92ea5588396b319ba43283a5a3087a034cb29c", "shasum": "" }, "require": { @@ -8719,7 +8728,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.19" + "source": "https://github.com/symfony/translation/tree/v6.4.21" }, "funding": [ { @@ -8735,7 +8744,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:18:43+00:00" + "time": "2025-04-07T19:02:30+00:00" }, { "name": "symfony/translation-contracts", @@ -8891,16 +8900,16 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.18", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", "shasum": "" }, "require": { @@ -8956,7 +8965,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" }, "funding": [ { @@ -8972,7 +8981,7 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:26:11+00:00" + "time": "2025-04-09T07:34:50+00:00" }, { "name": "theseer/tokenizer", @@ -9081,16 +9090,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -9149,7 +9158,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -9161,7 +9170,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "voku/portable-ascii", @@ -9515,20 +9524,20 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -9536,8 +9545,8 @@ "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -9560,9 +9569,9 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { "name": "mockery/mockery", @@ -10197,16 +10206,16 @@ }, { "name": "symfony/var-exporter", - "version": "v6.4.20", + "version": "v6.4.21", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "998df255e9e6a15a36ae35e9c6cd818c17cf92a2" + "reference": "717e7544aa99752c54ecba5c0e17459c48317472" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/998df255e9e6a15a36ae35e9c6cd818c17cf92a2", - "reference": "998df255e9e6a15a36ae35e9c6cd818c17cf92a2", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/717e7544aa99752c54ecba5c0e17459c48317472", + "reference": "717e7544aa99752c54ecba5c0e17459c48317472", "shasum": "" }, "require": { @@ -10254,7 +10263,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.20" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.21" }, "funding": [ { @@ -10270,7 +10279,7 @@ "type": "tidelift" } ], - "time": "2025-03-13T09:55:08+00:00" + "time": "2025-04-27T21:06:26+00:00" } ], "aliases": [], diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index 3b71ca6..7a3bef9 100755 Binary files a/flexiapi/composer.phar and b/flexiapi/composer.phar differ diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index c4da9ff..4a79527 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -31,6 +31,7 @@ return [ */ 'api_key_expiration_minutes' => env('APP_API_KEY_EXPIRATION_MINUTES', 60), 'account_creation_token_expiration_minutes' => env('APP_ACCOUNT_CREATION_TOKEN_EXPIRATION_MINUTES', 0), + 'account_recovery_token_expiration_minutes' => env('APP_ACCOUNT_RECOVERY_TOKEN_EXPIRATION_MINUTES', 0), 'email_change_code_expiration_minutes' => env('APP_EMAIL_CHANGE_CODE_EXPIRATION_MINUTES', 10), 'phone_change_code_expiration_minutes' => env('APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES', 10), 'recovery_code_expiration_minutes' => env('APP_RECOVERY_CODE_EXPIRATION_MINUTES', 10), diff --git a/flexiapi/database/factories/AccountRecoveryTokenFactory.php b/flexiapi/database/factories/AccountRecoveryTokenFactory.php new file mode 100644 index 0000000..dcc0dee --- /dev/null +++ b/flexiapi/database/factories/AccountRecoveryTokenFactory.php @@ -0,0 +1,53 @@ +. +*/ + +namespace Database\Factories; + +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Str; + +use App\AccountRecoveryToken; +use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; +use Illuminate\Support\Carbon; + +class AccountRecoveryTokenFactory extends Factory +{ + protected $model = AccountRecoveryToken::class; + + public function definition() + { + return [ + 'pn_provider' => $this->faker->uuid(), + 'pn_param' => $this->faker->uuid(), + 'pn_prid' => $this->faker->uuid(), + 'token' => Str::random(WebAuthenticateController::$emailCodeSize), + 'used' => false, + 'ip' => $this->faker->ipv4(), + 'user_agent' => $this->faker->userAgent(), + 'created_at' => Carbon::now() + ]; + } + + public function expired() + { + return $this->state(fn (array $attributes) => [ + 'created_at' => Carbon::now()->subMinutes(1000) + ]); + } +} diff --git a/flexiapi/database/migrations/2025_04_30_094148_create_account_recovery_tokens_table.php b/flexiapi/database/migrations/2025_04_30_094148_create_account_recovery_tokens_table.php new file mode 100644 index 0000000..ec1df9b --- /dev/null +++ b/flexiapi/database/migrations/2025_04_30_094148_create_account_recovery_tokens_table.php @@ -0,0 +1,44 @@ +string('phone')->nullable(); + $table->string('email')->nullable(); + }); + + Schema::create('account_recovery_tokens', function (Blueprint $table) { + $table->id(); + $table->string('token'); + $table->string('pn_provider'); + $table->string('pn_param'); + $table->string('pn_prid'); + $table->boolean('used')->default(false); + $table->string('ip')->nullable(); + $table->string('user_agent')->nullable(); + $table->integer('account_id')->unsigned()->nullable(); + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->timestamps(); + + $table->index('token'); + $table->index(['pn_provider', 'pn_param', 'pn_prid']); + }); + } + + public function down(): void + { + Schema::table('recovery_codes', function (Blueprint $table) { + $table->dropColumn('phone'); + $table->dropColumn('email'); + }); + + Schema::dropIfExists('account_recovery_tokens'); + } +}; diff --git a/flexiapi/lang/fr.json b/flexiapi/lang/fr.json index b4fd6e8..4895735 100644 --- a/flexiapi/lang/fr.json +++ b/flexiapi/lang/fr.json @@ -141,7 +141,7 @@ "Realm": "Royaume", "Record calls": "Enregistrements des appels", "Recover your account using your email": "Récupérer votre compte avec votre email", - "Recover your account using your phone number": "Récupérer votre compte avec votre numéro de téléphone", + "Use the mobile app to recover your account using your phone number": "Utilisez l'application mobile pour récupérer votre compte avec votre numéro de téléphone", "Register": "Inscription", "Registrar": "Registrar", "Registration introduction": "Présentation lors de l'inscription", @@ -198,6 +198,7 @@ "Username": "Identifiant", "Value": "Valeur", "Verify": "Vérifier", + "Via": "Via", "Week": "Semaine", "Welcome on :app_name": "Bienvenue sur :app_name", "Wrong username or password": "Mauvais identifiant ou mot de passe", diff --git a/flexiapi/resources/views/account/recovery/show.blade.php b/flexiapi/resources/views/account/recovery/show.blade.php index 9092b2b..26a2343 100644 --- a/flexiapi/resources/views/account/recovery/show.blade.php +++ b/flexiapi/resources/views/account/recovery/show.blade.php @@ -39,14 +39,18 @@

@endif
- + @include('parts.errors', ['name' => 'phone']) @include('parts.errors', ['name' => 'identifier'])
@endif - @include('parts.captcha') + @if (!empty($account_recovery_token)) + + @else + @include('parts.captcha') + @endif
diff --git a/flexiapi/resources/views/admin/account/activity/index.blade.php b/flexiapi/resources/views/admin/account/activity/index.blade.php index 6908d72..e4fe0ba 100644 --- a/flexiapi/resources/views/admin/account/activity/index.blade.php +++ b/flexiapi/resources/views/admin/account/activity/index.blade.php @@ -74,9 +74,9 @@
@endif - @if ($account->recoveryCodes->isNotEmpty()) -
-

Recovery Codes

+ @if ($account->accountRecoveryTokens) +
+

Account Recovery Tokens

@@ -84,11 +84,55 @@ + + @foreach ($account->accountRecoveryTokens as $key => $accountRecoveryToken) + + + + + @endforeach + +
{{ __('Used on') }}
+ {{ $accountRecoveryToken->created_at }} + consumed())class="crossed"@endif> + {{ __('Token') }}: {{ $accountRecoveryToken->token }} + + + {{ $accountRecoveryToken->created_at != $accountRecoveryToken->updated_at ? $accountRecoveryToken->updated_at : '-' }} + + IP: {{ $accountRecoveryToken->ip ?? '-' }} | + {{ \Illuminate\Support\Str::limit($accountRecoveryToken->user_agent, 20, $end='...') }} + +
+
+ @endif + + @if ($account->recoveryCodes->isNotEmpty()) +
+

Recovery Codes

+ + + + + + + + @foreach ($account->recoveryCodes as $key => $recoveryCode) +
{{ __('Created') }}{{ __('Via') }} phone/envelope{{ __('Used on') }}
{{ $recoveryCode->created_at }} + consumed())class="crossed"@endif> + {{ __('Code') }}: {{ $recoveryCode->code ?? '-' }} + + + @if ($recoveryCode->phone) + phone {{ $recoveryCode->phone }} + @elseif($recoveryCode->email) + envelope {{ $recoveryCode->email }} + @endif {{ $recoveryCode->created_at != $recoveryCode->updated_at ? $recoveryCode->updated_at : '-' }} @@ -120,10 +164,12 @@
{{ $phoneChangeCode->created_at }} + consumed())class="crossed"@endif> + {{ __('Code') }}: {{ $phoneChangeCode->code ?? '-' }} + {{ $phoneChangeCode->phone }} - {{ __('Code') }}: {{ $phoneChangeCode->code ?? '-' }} {{ $phoneChangeCode->created_at != $phoneChangeCode->updated_at ? $phoneChangeCode->updated_at : '-' }} @@ -155,10 +201,12 @@
{{ $emailChangeCode->created_at }} + consumed())class="crossed"@endif> + {{ __('Code') }}: {{ $emailChangeCode->code ?? '-' }} + {{ $emailChangeCode->email }} - {{ __('Code') }}: {{ $emailChangeCode->code ?? '-' }} {{ $emailChangeCode->created_at != $emailChangeCode->updated_at ? $emailChangeCode->updated_at : '-' }} @@ -189,6 +237,9 @@
{{ $provisioningToken->created_at }} + offed())class="crossed"@endif> + {{ $provisioningToken->token }} + {{ $provisioningToken->consumed() ? $provisioningToken->updated_at : '-' }} @@ -205,7 +256,7 @@ @endif @if ($account->resetPasswordEmailTokens->isNotEmpty()) -
+

{{ __('Reset password emails') }}

@@ -220,12 +271,12 @@
{{ $resetPasswordEmailToken->created_at }} + offed())class="crossed"@endif> + {{ $resetPasswordEmailToken->token }} + {{ $resetPasswordEmailToken->email }} - - {{ $resetPasswordEmailToken->token }} - {{ $resetPasswordEmailToken->consumed() ? $resetPasswordEmailToken->updated_at : '-' }} diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index 2189105..1e01cfb 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -121,15 +121,6 @@ You can find more documentation on the related [IETF RFC-7616](https://tools.iet Returns `pong` -## Account Creation Request Tokens - -An `account_creation_request_token` is a unique token that can be validated and then used to generate a valid `account_creation_token`. - -### `POST /account_creation_request_tokens` -Public - -Create and return an `account_creation_request_token` that should then be validated to be used. - ## Spaces Manage the list of allowed `spaces`. The admin accounts declared with a `domain` that is a `super` `sip_domain` will become Super Admin. @@ -249,6 +240,15 @@ JSON parameters: Delete the a space email server configuration. +## Account Creation Request Tokens + +An `account_creation_request_token` is a unique token that can be validated and then used to generate a valid `account_creation_token`. + +### `POST /account_creation_request_tokens` +Public + +Create and return an `account_creation_request_token` that should then be validated to be used. + ## Account Creation Tokens An `account_creation_token` is a unique token that allow the creation or the validation of a unique account. @@ -294,6 +294,28 @@ JSON parameters: Create and return an `account_creation_token`. +## Account Recovery Tokens + +An `account_recovery_token` is a unique token that allow the recovery of an account. + +It can be used on the following page that also accepts a `phone` optional parameter to prefil the recovery form: + + {{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_']) }} + {{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_', 'phone' => '+3312341234']) }} + +### `POST /account_recovery_tokens/send-by-push` +Public + +Create and send an `account_recovery_token` using a push notification to the device. +Return `403` if a token was already sent, or if the tokens limit is reached for this device. +Return `503` if the token was not successfully sent. + +JSON parameters: + +* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm +* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters +* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes and colon characters + ## Auth Tokens ### `POST /accounts/auth_token` diff --git a/flexiapi/resources/views/mails/recover_by_code.blade.php b/flexiapi/resources/views/mails/recover_by_code.blade.php index eedd5f7..0384d49 100644 --- a/flexiapi/resources/views/mails/recover_by_code.blade.php +++ b/flexiapi/resources/views/mails/recover_by_code.blade.php @@ -9,7 +9,7 @@ You are trying to authenticate to {{ $account->space->name }} using your email a Please enter the code bellow to finish the authentication process. -## {{ $account->recovery_code } +## {{ $account->recovery_code }} @if (config('app.recovery_code_expiration_minutes') > 0) The code is only available {{ config('app.recovery_code_expiration_minutes') }} minutes. diff --git a/flexiapi/resources/views/mails/recover_by_code_custom.blade.php.example b/flexiapi/resources/views/mails/recover_by_code_custom.blade.php.example index eedd5f7..0384d49 100644 --- a/flexiapi/resources/views/mails/recover_by_code_custom.blade.php.example +++ b/flexiapi/resources/views/mails/recover_by_code_custom.blade.php.example @@ -9,7 +9,7 @@ You are trying to authenticate to {{ $account->space->name }} using your email a Please enter the code bellow to finish the authentication process. -## {{ $account->recovery_code } +## {{ $account->recovery_code }} @if (config('app.recovery_code_expiration_minutes') > 0) The code is only available {{ config('app.recovery_code_expiration_minutes') }} minutes. diff --git a/flexiapi/resources/views/parts/recovery.blade.php b/flexiapi/resources/views/parts/recovery.blade.php index 5d1c283..311bb87 100644 --- a/flexiapi/resources/views/parts/recovery.blade.php +++ b/flexiapi/resources/views/parts/recovery.blade.php @@ -1,7 +1,7 @@

envelope{{ __('Recover your account using your email') }}
@if (space()->phone_registration) - phone{{ __('Recover your account using your phone number') }}
+ phone{{ __('Use the mobile app to recover your account using your phone number') }}
@endif qr-code{{ __('Login using a QRCode') }}

\ No newline at end of file diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 3677aba..3981c4f 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -44,6 +44,7 @@ Route::post('account_creation_request_tokens', 'Api\Account\CreationRequestToken Route::post('account_creation_tokens/send-by-push', 'Api\Account\CreationTokenController@sendByPush'); Route::post('account_creation_tokens/using-account-creation-request-token', 'Api\Account\CreationTokenController@usingAccountRequestToken'); Route::post('accounts/with-account-creation-token', 'Api\Account\AccountController@store'); +Route::post('account_recovery_tokens/send-by-push', 'Api\Account\RecoveryTokenController@sendByPush'); Route::get('accounts/{sip}/info', 'Api\Account\AccountController@info'); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 2d9c18b..c990d5d 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -100,7 +100,7 @@ Route::middleware(['web_panel_enabled', 'space.check'])->group(function () { }); Route::prefix('recovery')->controller(RecoveryController::class)->group(function () { - Route::get('phone', 'showPhone')->name('account.recovery.show.phone'); + Route::get('phone/{account_recovery_token}', 'showPhone')->name('account.recovery.show.phone'); Route::get('email', 'showEmail')->name('account.recovery.show.email'); Route::post('/', 'send')->name('account.recovery.send'); Route::post('confirm', 'confirm')->name('account.recovery.confirm'); diff --git a/flexiapi/tests/Feature/ApiAccountRecoveryTokenTest.php b/flexiapi/tests/Feature/ApiAccountRecoveryTokenTest.php new file mode 100644 index 0000000..48473ca --- /dev/null +++ b/flexiapi/tests/Feature/ApiAccountRecoveryTokenTest.php @@ -0,0 +1,95 @@ +. +*/ + +namespace Tests\Feature; + +use App\Account; +use App\Space; +use App\AccountRecoveryToken; +use Tests\TestCase; +use Carbon\Carbon; +use App\Http\Middleware\IsWebPanelEnabled; + +class ApiAccountRecoveryTokenTest extends TestCase +{ + protected $tokenRoute = '/api/account_recovery_tokens/send-by-push'; + protected $tokenRequestRoute = '/api/account_recovery_request_tokens'; + protected $method = 'POST'; + + protected $pnProvider = 'fcm'; + protected $pnParam = 'param'; + protected $pnPrid = 'id'; + + public function testMandatoryParameters() + { + $this->json($this->method, $this->tokenRoute)->assertStatus(422); + $this->json($this->method, $this->tokenRoute, [ + 'pn_provider' => null, + 'pn_param' => null, + 'pn_prid' => null, + ])->assertStatus(422); + } + + public function testThrottling() + { + AccountRecoveryToken::factory()->create([ + 'pn_provider' => $this->pnProvider, + 'pn_param' => $this->pnParam, + 'pn_prid' => $this->pnPrid, + ]); + + $this->json($this->method, $this->tokenRoute, [ + 'pn_provider' => $this->pnProvider, + 'pn_param' => $this->pnParam, + 'pn_prid' => $this->pnPrid, + ])->assertStatus(503); + + // Redeem all the tokens + AccountRecoveryToken::where('used', false)->update(['used' => true]); + + $this->json($this->method, $this->tokenRoute, [ + 'pn_provider' => $this->pnProvider, + 'pn_param' => $this->pnParam, + 'pn_prid' => $this->pnPrid, + ])->assertStatus(429); + } + + public function testTokenRecoveryPage() + { + $token = AccountRecoveryToken::factory()->create(); + $space = Space::factory()->create(); + $phone = '+3312345'; + + $this->get($this->setSpaceOnRoute($space, route('account.recovery.show.phone', ['account_recovery_token' => 'bad_token']))) + ->assertStatus(404); + + $this->get($this->setSpaceOnRoute($space, route('account.recovery.show.phone', ['account_recovery_token' => $token->token]))) + ->assertDontSee($phone) + ->assertStatus(200); + + $this->get($this->setSpaceOnRoute($space, route('account.recovery.show.phone', ['account_recovery_token' => $token->token, 'phone' => $phone]))) + ->assertSee($phone) + ->assertStatus(200); + + $token->consume(); + + $this->get($this->setSpaceOnRoute($space, route('account.recovery.show.phone', ['account_recovery_token' => $token->token]))) + ->assertStatus(404); + } +} diff --git a/flexiapi/tests/TestCase.php b/flexiapi/tests/TestCase.php index 91223ae..bb1d122 100644 --- a/flexiapi/tests/TestCase.php +++ b/flexiapi/tests/TestCase.php @@ -20,6 +20,7 @@ namespace Tests; use App\PhoneCountry; +use App\Space; use App\Http\Middleware\SpaceCheck; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -45,4 +46,9 @@ abstract class TestCase extends BaseTestCase PhoneCountry::factory()->france()->activated()->create(); PhoneCountry::factory()->netherlands()->create(); } + + protected function setSpaceOnRoute(Space $space, string $route) + { + return str_replace('localhost', $space->domain, $route); + } }