Fix FLEXIAPI-286 Send an account_recovery_token using a push notification

This commit is contained in:
Timothée Jaussoin 2025-05-05 13:43:44 +00:00
parent 682b0ae67b
commit cd3b9b818b
26 changed files with 604 additions and 169 deletions

View file

@ -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
----

View file

@ -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

View file

@ -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

View file

@ -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()) {

View file

@ -0,0 +1,42 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AccountRecoveryToken extends Consommable
{
use HasFactory;
protected $hidden = ['id', 'updated_at', 'created_at'];
protected $appends = ['expire_at'];
protected ?string $configExpirationMinutesKey = 'account_recovery_token_expiration_minutes';
public function account()
{
return $this->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,
];
}
}

View file

@ -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', [

View file

@ -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();

View file

@ -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;

View file

@ -0,0 +1,75 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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");
}
}

View file

@ -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;
}

251
flexiapi/composer.lock generated
View file

@ -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": [],

Binary file not shown.

View file

@ -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),

View file

@ -0,0 +1,53 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2021 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
]);
}
}

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('recovery_codes', function (Blueprint $table) {
$table->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');
}
};

View file

@ -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",

View file

@ -39,14 +39,18 @@
</p>
@endif
<div>
<input placeholder="+123456789" name="phone" type="text" value="{{ old('phone') }}">
<input placeholder="+123456789" name="phone" type="text" value="@if ($phone){{ $phone }}@else{{ old('phone') }}@endif">
<label for="phone">{{ __('Phone number') }}</label>
@include('parts.errors', ['name' => 'phone'])
@include('parts.errors', ['name' => 'identifier'])
</div>
@endif
@include('parts.captcha')
@if (!empty($account_recovery_token))
<input name="account_recovery_token" type="hidden" value="{{ $account_recovery_token }}">
@else
@include('parts.captcha')
@endif
<div class="large">
<input class="btn oppose" type="submit" value="{{ __('Send')}}">

View file

@ -74,9 +74,9 @@
</div>
@endif
@if ($account->recoveryCodes->isNotEmpty())
<div class="card">
<h3>Recovery Codes</h3>
@if ($account->accountRecoveryTokens)
<div class="card large">
<h3>Account Recovery Tokens</h3>
<table>
<thead>
<tr>
@ -84,11 +84,55 @@
<th>{{ __('Used on') }}</th>
</tr>
</thead>
<tbody>
@foreach ($account->accountRecoveryTokens as $key => $accountRecoveryToken)
<tr>
<td>
{{ $accountRecoveryToken->created_at }}
<small @if ($accountRecoveryToken->consumed())class="crossed"@endif>
{{ __('Token') }}: {{ $accountRecoveryToken->token }}
</small>
</td>
<td>
{{ $accountRecoveryToken->created_at != $accountRecoveryToken->updated_at ? $accountRecoveryToken->updated_at : '-' }}
<small title="{{ $accountRecoveryToken->user_agent }}">
IP: {{ $accountRecoveryToken->ip ?? '-' }} |
{{ \Illuminate\Support\Str::limit($accountRecoveryToken->user_agent, 20, $end='...') }}
</small>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if ($account->recoveryCodes->isNotEmpty())
<div class="card large">
<h3>Recovery Codes</h3>
<table>
<thead>
<tr>
<th>{{ __('Created') }}</th>
<th>{{ __('Via') }} <i class="ph">phone</i>/<i class="ph">envelope</i></th>
<th>{{ __('Used on') }}</th>
</tr>
</thead>
<tbody>
@foreach ($account->recoveryCodes as $key => $recoveryCode)
<tr>
<td>
{{ $recoveryCode->created_at }}
<small @if ($recoveryCode->consumed())class="crossed"@endif>
{{ __('Code') }}: {{ $recoveryCode->code ?? '-' }}
</small>
</td>
<td>
@if ($recoveryCode->phone)
<i class="ph">phone</i> {{ $recoveryCode->phone }}
@elseif($recoveryCode->email)
<i class="ph">envelope</i> {{ $recoveryCode->email }}
@endif
</td>
<td>
{{ $recoveryCode->created_at != $recoveryCode->updated_at ? $recoveryCode->updated_at : '-' }}
@ -120,10 +164,12 @@
<tr>
<td>
{{ $phoneChangeCode->created_at }}
<small @if ($phoneChangeCode->consumed())class="crossed"@endif>
{{ __('Code') }}: {{ $phoneChangeCode->code ?? '-' }}
</small>
</td>
<td>
{{ $phoneChangeCode->phone }}
<small>{{ __('Code') }}: {{ $phoneChangeCode->code ?? '-' }}</small>
</td>
<td title="{{ $phoneChangeCode->user_agent }}">
{{ $phoneChangeCode->created_at != $phoneChangeCode->updated_at ? $phoneChangeCode->updated_at : '-' }}
@ -155,10 +201,12 @@
<tr>
<td>
{{ $emailChangeCode->created_at }}
<small @if ($emailChangeCode->consumed())class="crossed"@endif>
{{ __('Code') }}: {{ $emailChangeCode->code ?? '-' }}
</small>
</td>
<td>
{{ $emailChangeCode->email }}
<small>{{ __('Code') }}: {{ $emailChangeCode->code ?? '-' }}</small>
</td>
<td title="{{ $emailChangeCode->user_agent }}">
{{ $emailChangeCode->created_at != $emailChangeCode->updated_at ? $emailChangeCode->updated_at : '-' }}
@ -189,6 +237,9 @@
<tr>
<td>
{{ $provisioningToken->created_at }}
<small @if ($provisioningToken->offed())class="crossed"@endif>
{{ $provisioningToken->token }}
</small>
</td>
<td>
{{ $provisioningToken->consumed() ? $provisioningToken->updated_at : '-' }}
@ -205,7 +256,7 @@
@endif
@if ($account->resetPasswordEmailTokens->isNotEmpty())
<div class="card">
<div class="card large">
<h3>{{ __('Reset password emails') }}</h3>
<table>
<thead>
@ -220,12 +271,12 @@
<tr>
<td>
{{ $resetPasswordEmailToken->created_at }}
<small @if ($resetPasswordEmailToken->offed())class="crossed"@endif>
{{ $resetPasswordEmailToken->token }}
</small>
</td>
<td>
{{ $resetPasswordEmailToken->email }}
<small>
{{ $resetPasswordEmailToken->token }}
</small>
</td>
<td>
{{ $resetPasswordEmailToken->consumed() ? $resetPasswordEmailToken->updated_at : '-' }}

View file

@ -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`
<span class="badge badge-success">Public</span>
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 <span class="badge badge-error">Super Admin</span>.
@ -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`
<span class="badge badge-success">Public</span>
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`
<span class="badge badge-success">Public</span>
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`

View file

@ -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.

View file

@ -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.

View file

@ -1,7 +1,7 @@
<p class="text-center pt-3">
<i class="ph">envelope</i><a href="{{ route('account.recovery.show.email') }}">{{ __('Recover your account using your email') }}</a><br />
@if (space()->phone_registration)
<i class="ph">phone</i><a href="{{ route('account.recovery.show.phone') }}">{{ __('Recover your account using your phone number') }}</a><br />
<i class="ph">phone</i>{{ __('Use the mobile app to recover your account using your phone number') }}<br />
@endif
<i class="ph">qr-code</i><a href="{{ route('account.authenticate.auth_token') }}">{{ __('Login using a QRCode') }}</a>
</p>

View file

@ -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');

View file

@ -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');

View file

@ -0,0 +1,95 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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);
}
}

View file

@ -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);
}
}