Fix FLEXIAPI-205 Remove the deprecated endpoints, compatibility code...

This commit is contained in:
Timothée Jaussoin 2025-04-22 12:51:15 +00:00
parent a3861304cc
commit e2f40699fb
23 changed files with 47 additions and 993 deletions

View file

@ -2,6 +2,7 @@
v1.7
----
- Fix FLEXIAPI-205 Remove the deprecated endpoints, compatibility code documentation and tests. Drop the confirmation_key accounts column and activation_expirations table
- Fix FLEXIAPI-206 Upgrade to Laravel 10, PHP 8.1 minimum and bump all the related dependencies, drop Debian 11 Bullseye
- Fix FLEXIAPI-220 Migrate SIP Domains to Spaces
- Fix GH-15 Add password import from CSV

View file

@ -104,9 +104,6 @@ You can also seed the tables with test accounts for the liblinphone test suite w
To send SMS to the USA some providers need to validate their templates before transfering them, see [Sending SMS messages to the USA - OVH](https://help.ovhcloud.com/csm/en-ie-sms-sending-sms-to-usa?id=kb_article_view&sysparm_article=KB0051359).
Here are the currently used SMS templates in the app to declare in your provider panel:
- Creation code: `Your #APP_NAME# creation code is #CODE#`. Sent to confirm the creation of the account by SMS.
- Recovery code: `Your #APP_NAME# recovery code is #CODE#`. Sent to recover the account by SMS.
- Validation code: `Your #APP_NAME# validation code is #CODE#`. Sent to validate the phone change by SMS.
- Validation code with expiration: `Your #APP_NAME# validation code is #CODE#. The code is available for #CODE_MINUTES# minutes`. Sent to validate the phone change by SMS, include an expiration time.

View file

@ -36,9 +36,9 @@ class Account extends Authenticatable
use HasFactory;
use Compoships;
protected $with = ['passwords', 'activationExpiration', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries'];
protected $hidden = ['expire_time', 'confirmation_key', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries'];
protected $appends = ['realm', 'confirmation_key_expires', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary'];
protected $with = ['passwords', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries'];
protected $hidden = ['expire_time', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries'];
protected $appends = ['realm', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary'];
protected $casts = [
'activated' => 'boolean',
];
@ -111,11 +111,6 @@ class Account extends Authenticatable
});
}
public function activationExpiration()
{
return $this->hasOne(ActivationExpiration::class);
}
public function apiKey()
{
return $this->hasOne(ApiKey::class)->whereNull('expires_after_last_used_minutes');
@ -352,10 +347,6 @@ class Account extends Authenticatable
/**
* Utils
*/
public function activationExpired(): bool
{
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function generateUserApiKey(?string $ip = null): ApiKey
{

View file

@ -1,44 +0,0 @@
<?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;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ActivationExpiration extends Model
{
use HasFactory;
protected $casts = [
'expires' => 'datetime:Y-m-d H:i:s',
];
public function account()
{
return $this->belongsTo(Account::class);
}
public function isExpired()
{
$now = Carbon::now();
return $this->expires->lessThan($now);
}
}

View file

@ -80,29 +80,6 @@ class AuthenticateController extends Controller
return redirect()->back()->withErrors(['authentication' => __('Wrong username or password')]);
}
/**
* Deprecated
*/
public function validateEmail(Request $request, string $code)
{
$request->merge(['code' => $code]);
$request->validate(['code' => 'required|size:' . self::$emailCodeSize]);
$account = Account::where('confirmation_key', $code)->first();
if (!$account) {
return redirect()->route('account.login');
}
$account->confirmation_key = null;
$account->activated = true;
$account->save();
Auth::login($account);
return redirect()->route('account.home');
}
public function loginAuthToken(Request $request, ?string $token = null)
{
$authToken = null;

View file

@ -51,10 +51,6 @@ class ProvisioningController extends Controller
})
->firstOrFail();
if ($account->activationExpired()) {
abort(404);
}
$params = ['provisioning_token' => $provisioningToken];
if ($request->has('reset_password')) {
@ -130,7 +126,7 @@ class ProvisioningController extends Controller
})
->firstOrFail();
if ($account->activationExpired() || ($provisioningToken != $account->provisioning_token)) {
if ($provisioningToken != $account->provisioning_token) {
return abort(404);
}

View file

@ -20,29 +20,12 @@
namespace App\Http\Controllers\Api\Account;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use App\Http\Requests\Account\Create\Api\Request as ApiRequest;
use App\Account;
use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Http\Requests\Account\Create\Api\Request as ApiRequest;
use App\Libraries\OvhSMS;
use App\Mail\RegisterConfirmation;
use App\Rules\AccountCreationToken as RulesAccountCreationToken;
use App\Rules\AccountCreationTokenNotExpired;
use App\Rules\BlacklistedUsername;
use App\Rules\FilteredPhone;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\PasswordAlgorithm;
use App\Services\AccountService;
class AccountController extends Controller
@ -81,256 +64,11 @@ class AccountController extends Controller
return abort(404, 'No TURN service configured');
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function phoneInfo(Request $request, string $phone)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->merge(['phone' => $phone]);
$request->validate([
'phone' => ['required', 'phone', new FilteredPhone]
]);
$account = Account::where('domain', config('app.sip_domain'))
->where(function ($query) use ($phone) {
$query->where('username', $phone)
->orWhere('phone', $phone);
})->firstOrFail();
return \response()->json([
'activated' => $account->activated,
'realm' => $account->realm,
'phone' => (bool)$account->phone
]);
}
/**
* /!\ Dangerous endpoint, disabled by default
* Store directly the account and alias in the DB and send a SMS or email for the validation
*/
public function storePublic(Request $request)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->validate([
'username' => [
'required_without:phone',
new NoUppercase,
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
$query->where('domain', $request->has('domain') ? $request->get('domain') : config('app.sip_domain'));
}),
Rule::unique('accounts_tombstones', 'username')->where(function ($query) use ($request) {
$query->where('domain', $request->has('domain') ? $request->get('domain') : config('app.sip_domain'));
}),
'filled',
],
'algorithm' => ['required', new PasswordAlgorithm],
'password' => 'required|filled',
'domain' => 'min:3',
'email' => config('app.account_email_unique')
? 'required_without:phone|email|unique:accounts,email'
: 'required_without:phone|email',
'phone' => [
'required_without:email',
'required_without:username',
'phone',
new FilteredPhone,
'unique:accounts,phone',
'unique:accounts,username',
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken,
new AccountCreationTokenNotExpired
]
]);
$account = new Account;
$account->username = !empty($request->get('username'))
? $request->get('username')
: $request->get('phone');
$account->email = $request->get('email');
$account->activated = false;
$account->domain = $request->has('domain')
? $request->get('domain')
: config('app.sip_domain');
$account->ip_address = $request->ip();
$account->created_at = Carbon::now();
$account->user_agent = $request->header('User-Agent') ?? space()->name;
$account->save();
$account->updatePassword($request->get('password'), $request->get('algorithm'));
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
$token->consume();
$token->account_id = $account->id;
$token->save();
Log::channel('events')->info('API deprecated - Store public: AccountCreationToken redeemed', ['account_creation_token' => $token->toLog()]);
Log::channel('events')->info('API deprecated - Store public: Account created', ['id' => $account->identifier]);
// Send validation by phone
if ($request->has('phone')) {
$account->phone = $request->get('phone');
$account->confirmation_key = generatePin();
$account->save();
Log::channel('events')->info('API deprecated: Account created using the public endpoint by phone', ['id' => $account->identifier]);
$ovhSMS = new OvhSMS;
$ovhSMS->send($request->get('phone'), 'Your ' . space()->name . ' creation code is ' . $account->confirmation_key);
} elseif ($request->has('email')) {
// Send validation by email
$account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize);
$account->save();
Log::channel('events')->info('API deprecated - Store public: Account created using the public endpoint by email', ['id' => $account->identifier]);
try {
Mail::to($account)->send(new RegisterConfirmation($account));
} catch (\Exception $e) {
Log::channel('events')->info('API deprecated - Store public: Public Register Confirmation email not sent, check errors log', ['id' => $account->identifier]);
Log::error('Public Register Confirmation email not sent: ' . $e->getMessage());
}
}
// Full reload
return Account::withoutGlobalScopes()->find($account->id);
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function recoverByPhone(Request $request)
{
if (!config('app.dangerous_endpoints')) return abort(404);
$request->validate([
'phone' => [
'required', 'phone', new FilteredPhone, 'exists:accounts,phone'
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken,
new AccountCreationTokenNotExpired
]
]);
$account = Account::where('phone', $request->get('phone'))->first();
$account->confirmation_key = generatePin();
$account->save();
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
$token->consume();
$token->account_id = $account->id;
$token->save();
Log::channel('events')->info('API deprecated - Account recovery: AccountCreationToken redeemed', ['account_creation_token' => $token->toLog()]);
Log::channel('events')->info('API deprecated - Account recovery: Account recovery by phone', ['id' => $account->identifier]);
$ovhSMS = new OvhSMS;
$ovhSMS->send($request->get('phone'), 'Your ' . space()->name . ' recovery code is ' . $account->confirmation_key);
}
/**
* /!\ Dangerous endpoint, disabled by default
*/
public function recoverUsingKey(string $sip, string $recoveryKey)
{
if (!config('app.dangerous_endpoints')) return abort(404);
list($username, $domain) = explode('@', $sip);
$account = Account::where('domain', $domain)
->where(function ($query) use ($username) {
$query->where('username', $username)
->orWhere('phone', $username);
})->firstOrFail();
$confirmationKey = $account->confirmation_key;
$account->confirmation_key = null;
if ($confirmationKey != $recoveryKey) abort(404);
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->save();
$account->passwords->each(function ($i, $k) {
$i->makeVisible(['password']);
});
return $account;
}
public function store(ApiRequest $request)
{
return (new AccountService)->store($request);
}
/**
* Deprecated
*/
public function activateEmail(Request $request, string $sip)
{
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([
'confirmation_key' => 'required|size:' . WebAuthenticateController::$emailCodeSize
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();
Log::channel('events')->info('API: Account activated by email', ['id' => $account->identifier]);
return $account;
}
/**
* Deprecated
*/
public function activatePhone(Request $request, string $sip)
{
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([
'confirmation_key' => 'required|digits:4'
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();
Log::channel('events')->info('API: Account activated by phone', ['id' => $account->identifier]);
return $account;
}
public function show(Request $request)
{
return Account::where('id', $request->user()->id)

View file

@ -156,7 +156,7 @@ class AccountController extends Controller
Log::channel('events')->info('API Admin: Account updated', ['id' => $account->identifier]);
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
return $account->makeVisible(['provisioning_token']);
}
public function typeAdd(int $accountId, int $typeId)

View file

@ -36,10 +36,6 @@ class AsAdminRequest extends Request
$rules['algorithm'] = ['required', new PasswordAlgorithm()];
$rules['admin'] = 'boolean|nullable';
$rules['activated'] = 'boolean|nullable';
$rules['confirmation_key_expires'] = [
'date_format:Y-m-d H:i:s',
'nullable',
];
if (config('app.allow_phone_number_username_admin_api') == true) {
array_splice(

View file

@ -1,47 +0,0 @@
<?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\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Account;
class RegisterConfirmation extends Mailable
{
use Queueable, SerializesModels;
private $account;
public function __construct(Account $account)
{
$this->account = $account;
}
public function build()
{
return $this->view('mails.register_confirmation')
->text('mails.register_confirmation_text')
->with([
'link' => route('account.authenticate.email_confirm', [$this->account->confirmation_key])
]);
}
}

View file

@ -21,9 +21,7 @@ namespace App\Services;
use App\Account;
use App\AccountCreationToken;
use App\ActivationExpiration;
use App\EmailChangeCode;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Http\Requests\Account\Create\Request as CreateRequest;
use App\Http\Requests\Account\Update\Request as UpdateRequest;
use App\Libraries\OvhSMS;
@ -38,7 +36,6 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;
class AccountService
{
@ -79,21 +76,9 @@ class AccountService
}
}
if ($account->activated == false) {
$account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize);
}
$account->save();
if ($request->asAdmin) {
if ((!$request->has('activated') || !(bool)$request->get('activated'))
&& $request->has('confirmation_key_expires')) {
$actionvationExpiration = new ActivationExpiration();
$actionvationExpiration->account_id = $account->id;
$actionvationExpiration->expires = $request->get('confirmation_key_expires');
$actionvationExpiration->save();
}
if ($request->has('dictionary')) {
foreach ($request->get('dictionary') as $key => $value) {
$account->setDictionaryEntry($key, $value);

View file

@ -76,11 +76,6 @@ class BlockingService
Carbon::now()->subMinutes(config('app.blocking_time_period_check'))->toDateTimeString()
)->count();
// Deprecated, also detect if the account itself was updated recently, might be because of the confirmation_key change
if (Carbon::now()->subMinutes(config('app.blocking_time_period_check'))->isBefore($this->account->updated_at)) {
$events++;
}
return $events;
}
}

View file

@ -61,11 +61,6 @@ return [
'blocking_time_period_check' => env('BLOCKING_TIME_PERIOD_CHECK', 30),
'blocking_amount_events_authorized_during_period' => env('BLOCKING_AMOUNT_EVENTS_AUTHORIZED_DURING_PERIOD', 5),
/**
* /!\ Enable dangerous endpoints required for fallback
*/
'dangerous_endpoints' => env('APP_DANGEROUS_ENDPOINTS', false),
/*
|--------------------------------------------------------------------------
| Application Environment

View file

@ -44,7 +44,6 @@ class AccountFactory extends Factory
'display_name' => $this->faker->name,
'domain' => $domain->domain,
'user_agent' => $this->faker->userAgent,
'confirmation_key' => Str::random(WebAuthenticateController::$emailCodeSize),
'ip_address' => $this->faker->ipv4,
'created_at' => $this->faker->dateTimeBetween('-1 year'),
'dtmf_protocol' => array_rand(Account::$dtmfProtocols),

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::dropIfExists('activation_expirations');
Schema::table('accounts', function (Blueprint $table) {
$table->dropColumn('confirmation_key');
});
}
public function down()
{
Schema::table('accounts', function (Blueprint $table) {
$table->string('confirmation_key', 14)->nullable();
});
Schema::create('activation_expirations', function (Blueprint $table) {
$table->id();
$table->integer('account_id')->unsigned();
$table->dateTime('expires');
$table->timestamps();
$table->foreign('account_id')->references('id')
->on('accounts')->onDelete('cascade');
});
}
};

View file

@ -41,7 +41,6 @@ class LiblinphoneTesterAccoutSeeder extends Seeder
$element->domain,
$element->phone ?? null,
$element->activated ?? true,
$element->confirmation_key ?? null
)
);
@ -71,7 +70,6 @@ class LiblinphoneTesterAccoutSeeder extends Seeder
$element->domain,
$element->phone ?? null,
$element->activated ?? true,
$element->confirmation_key ?? null
)
);
@ -104,7 +102,7 @@ class LiblinphoneTesterAccoutSeeder extends Seeder
private function generateAccountArray(
int $id, string $username, string $domain, string $phone = null,
bool $activated = true, string $confirmationKey = null
bool $activated = true
): array {
return [
'id' => $id,
@ -114,7 +112,6 @@ class LiblinphoneTesterAccoutSeeder extends Seeder
'email' => rawurlencode($username) . '@' . $domain,
'activated' => $activated,
'ip_address' => '',
'confirmation_key' => $confirmationKey,
'user_agent' => 'FlexiAPI Seeder',
'created_at' => '2010-01-03 04:30:43'
];

View file

@ -312,23 +312,6 @@ Return `404` if the token is non existing or invalid.
## Accounts
### `POST /accounts/public`
<span class="badge badge-message">Deprecated</span> @if(!config('app.dangerous_endpoints'))<span class="badge">Disabled</span>@endif <span class="badge badge-success">Public</span> <span class="badge badge-error">Unsecure endpoint</span>
Create an account.
Return `422` if the parameters are invalid.
Send an email with the activation key if `email` is set, send an SMS otherwise.
JSON parameters:
* `username` **required** if `phone` not set, unique username, minimum 6 characters
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `domain` if not set the value is enforced to the default registration domain set in the global configuration
* `email` optional if `phone` set, an email, set an email to the account, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `phone` **required** if `username` not set, optional if `email` set, a valid phone number, set a phone number to the account
* `account_creation_token` the unique `account_creation_token`
### `POST /accounts/with-account-creation-token`
<span class="badge badge-success">Public</span>
@ -352,62 +335,6 @@ JSON parameters:
Retrieve public information about the account.
Return `404` if the account doesn't exists.
### `GET /accounts/{phone}/info-by-phone`
<span class="badge badge-message">Deprecated</span> @if(!config('app.dangerous_endpoints'))<span class="badge">Disabled</span>@endif <span class="badge badge-success">Public</span> <span class="badge badge-error">Unsecure endpoint</span>
Retrieve public information about the account.
Return `404` if the account doesn't exists.
Return `phone: true` if the returned account has a phone number.
### `POST /accounts/recover-by-phone`
<span class="badge badge-message">Deprecated</span> @if(!config('app.dangerous_endpoints'))<span class="badge">Disabled</span>@endif <span class="badge badge-success">Public</span> <span class="badge badge-error">Unsecure endpoint</span>
Send a SMS with a recovery PIN code to the `phone` number provided.
Return `404` if the account doesn't exists.
Can only be used once, a new `recover_key` need to be requested to be called again.
JSON parameters:
* `phone` **required**, the phone number to send the SMS to
* `account_creation_token` the unique `account_creation_token`
### `GET /accounts/{sip}/recover/{recover_key}`
<span class="badge badge-message">Deprecated</span> @if(!config('app.dangerous_endpoints'))<span class="badge">Disabled</span>@endif <span class="badge badge-success">Public</span> <span class="badge badge-error">Unsecure endpoint</span>
Activate the account if the correct `recover_key` is provided.
The `sip` parameter can be the default SIP account or the phone based one.
Return the account information (including the hashed password) if valid.
Return `404` if the account doesn't exists.
### `POST /accounts/{sip}/activate/email`
<span class="badge badge-message">Deprecated</span> <span class="badge badge-success">Public</span>
<a href="#post-accountsmeemailrequest">Use `POST /accounts/me/email/request` instead</a>.
Activate an account using a secret code received by email.
Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
JSON parameters:
* `confirmation_key` the confirmation key
### `POST /accounts/{sip}/activate/phone`
<span class="badge badge-message">Deprecated</span> <span class="badge badge-success">Public</span>
<a href="#post-accountsmephonerequest">Use `POST /accounts/me/phone/request` instead</a>.
Activate an account using a pin code received by phone.
Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
JSON parameters:
* `confirmation_key` the PIN code
### `GET /accounts/me/api_key/{auth_token}`
<span class="badge badge-success">Public</span>
@ -459,7 +386,7 @@ JSON parameters:
### `POST /accounts`
<span class="badge badge-warning">Admin</span>
To create an account directly from the API. <span class="badge badge-message">Deprecated</span> If `activated` is set to `false` a random generated `confirmation_key` and `provisioning_token` will be returned to allow further activation using the public endpoints and provision the account. Check `confirmation_key_expires` to also set an expiration date on that `confirmation_key`.
To create an account directly from the API.
Return `403` if the `max_accounts` limit of the corresponding Space is reached.
@ -476,7 +403,6 @@ JSON parameters:
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
* <span class="badge badge-message">Deprecated</span> `confirmation_key_expires` optional, a datetime of this format: Y-m-d H:i:s. Only used when `activated` is not used or `false`. Enforces an expiration date on the returned `confirmation_key`. After that datetime public email or phone activation endpoints will return `403`.
### `PUT /accounts/{id}`
<span class="badge badge-warning">Admin</span>

View file

@ -1,19 +0,0 @@
<html>
<head>
<title>Register on {{ space()->name }}</title>
</head>
<body>
<p>Hello,</p>
<p>
You just created an account on {{ space()->name }} using your email account.<br />
Please follow the unique link bellow to set up your password and finish the registration process.
</p>
<p>
<a href="{{ $link }}">{{ $link }}</a>
</p>
<p>
Regards,<br />
{{ config('mail.signature') }}
</p>
</body>
</html>

View file

@ -1,9 +0,0 @@
Hello,
You just created an account on {{ space()->name }} using your email account.
Please follow the unique link bellow to set up your password and finish the registration process.
{{ $link }}
Regards,
{{ config('mail.signature') }}

View file

@ -47,16 +47,6 @@ Route::post('accounts/with-account-creation-token', 'Api\Account\AccountControll
Route::get('accounts/{sip}/info', 'Api\Account\AccountController@info');
// Deprecated endpoints
Route::post('accounts/{sip}/activate/email', 'Api\Account\AccountController@activateEmail');
Route::post('accounts/{sip}/activate/phone', 'Api\Account\AccountController@activatePhone');
// Deprecated endpoints /!\ Dangerous endpoints
Route::post('accounts/public', 'Api\Account\AccountController@storePublic');
Route::get('accounts/{sip}/recover/{recovery_key}', 'Api\Account\AccountController@recoverUsingKey');
Route::post('accounts/recover-by-phone', 'Api\Account\AccountController@recoverByPhone');
Route::get('accounts/{phone}/info-by-phone', 'Api\Account\AccountController@phoneInfo');
Route::post('accounts/auth_token', 'Api\Account\AuthTokenController@store');
Route::get('accounts/me/api_key/{auth_token}', 'Api\Account\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt');

View file

@ -60,9 +60,6 @@ Route::middleware(['web_panel_enabled', 'space.check'])->group(function () {
Route::get('reset_password/{token}', 'Account\ResetPasswordEmailController@change')->name('account.reset_password_email.change');
Route::post('reset_password', 'Account\ResetPasswordEmailController@reset')->name('account.reset_password_email.reset');
// Deprecated
Route::get('authenticate/email/{code}', 'Account\AuthenticateController@validateEmail')->name('account.authenticate.email_confirm');
Route::prefix('creation_token')->controller(CreationRequestTokenController::class)->group(function () {
Route::get('check/{token}', 'check')->name('account.creation_request_token.check');
Route::post('validate', 'validateToken')->name('account.creation_request_token.validate');

View file

@ -32,7 +32,7 @@ class AccountBlockingTest extends TestCase
$account = Account::factory()->withConsumedAccountCreationToken()->create();
$account->generateUserApiKey();
config()->set('app.blocking_amount_events_authorized_during_period', 2);
config()->set('app.blocking_amount_events_authorized_during_period', 1);
$this->keyAuthenticated($account)
->json($this->method, $this->route . '/me/phone/request', [

View file

@ -20,9 +20,7 @@
namespace Tests\Feature;
use App\Account;
use App\AccountCreationToken;
use App\AccountTombstone;
use App\ActivationExpiration;
use App\Password;
use App\Space;
use Carbon\Carbon;
@ -182,7 +180,6 @@ class ApiAccountTest extends TestCase
'activated' => false
]);
$this->assertFalse(empty($response1['confirmation_key']));
$this->assertFalse(empty($response1['provisioning_token']));
}
@ -327,7 +324,6 @@ class ApiAccountTest extends TestCase
'activated' => false
]);
$this->assertFalse(empty($response1['confirmation_key']));
$this->assertFalse(empty($response1['provisioning_token']));
}
@ -394,7 +390,6 @@ class ApiAccountTest extends TestCase
'admin' => true,
]);
$this->assertTrue(!empty($response1['confirmation_key']));
$this->assertFalse(empty($response1['provisioning_token']));
}
@ -532,15 +527,13 @@ class ApiAccountTest extends TestCase
$username = 'username';
$response0 = $this->generateFirstResponse($password);
$response1 = $this->generateSecondResponse($password, $response0)
$this->generateSecondResponse($password, $response0)
->json($this->method, $this->route, [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => 'blabla',
'activated' => true,
]);
$response1
])
->assertStatus(200)
->assertJson([
'id' => 2,
@ -548,8 +541,6 @@ class ApiAccountTest extends TestCase
'domain' => config('app.sip_domain'),
'activated' => true,
]);
$this->assertTrue(empty($response1['confirmation_key']));
}
public function testNotActivated()
@ -575,7 +566,6 @@ class ApiAccountTest extends TestCase
'activated' => false,
]);
$this->assertFalse(empty($response1['confirmation_key']));
$this->assertFalse(empty($response1['provisioning_token']));
}
@ -630,68 +620,6 @@ class ApiAccountTest extends TestCase
->assertStatus(404);
}
public function testActivateEmail()
{
$confirmationKey = '0123456789abc';
$password = Password::factory()->create();
$password->account->generateUserApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->activated = false;
$password->account->save();
$expiration = new ActivationExpiration();
$expiration->account_id = $password->account->id;
$expiration->expires = Carbon::now()->subYear();
$expiration->save();
$this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200)
->assertJson([
'activated' => false
]);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route . '/blabla/activate/email', [
'confirmation_key' => $confirmationKey
])
->assertStatus(404);
$activateEmailRoute = $this->route . '/' . $password->account->identifier . '/activate/email';
$this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [
'confirmation_key' => $confirmationKey . 'longer'
])
->assertStatus(422);
$this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [
'confirmation_key' => 'X123456789abc'
])
->assertStatus(404);
// Expired
$this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [
'confirmation_key' => $confirmationKey
])
->assertStatus(403);
$expiration->delete();
$this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [
'confirmation_key' => $confirmationKey
])
->assertStatus(200);
$this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200)
->assertJson([
'activated' => true
]);
}
public function testUniqueEmailAdmin()
{
$email = 'collision@email.com';
@ -794,329 +722,6 @@ class ApiAccountTest extends TestCase
]);
}
/**
* /!\ Dangerous endpoints
*/
public function testRecover()
{
$confirmationKey = '0123';
$password = Password::factory()->create();
$password->account->generateUserApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->activated = false;
$password->account->save();
config()->set('app.dangerous_endpoints', true);
$this->assertDatabaseHas('accounts', [
'username' => $password->account->username,
'domain' => $password->account->domain,
'activated' => false
]);
$this->get($this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
->assertJson(['passwords' => [[
'password' => $password->password,
'algorithm' => $password->algorithm
]]])
->assertStatus(200);
$this->json('GET', $this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
->assertStatus(404);
$this->assertDatabaseHas('accounts', [
'username' => $password->account->username,
'domain' => $password->account->domain,
'confirmation_key' => null,
'activated' => true
]);
// Recover by phone
$newConfirmationKey = '1345';
$phone = '+1234';
$password->account->confirmation_key = $newConfirmationKey;
$password->account->phone = $phone;
$password->account->save();
$this->get($this->route . '/' . $phone . '@' . $password->account->domain . '/recover/' . $newConfirmationKey)
->assertJson(['passwords' => [[
'password' => $password->password,
'algorithm' => $password->algorithm
]]])
->assertStatus(200);
}
public function testRecoverTwice()
{
$confirmationKey = '1234';
$password = Password::factory()->create();
$password->account->generateUserApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->activated = false;
$password->account->save();
$this->get($this->route . '/' . $password->account->identifier . '/recover/wrongkey')
->assertStatus(404);
$this->get($this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
->assertStatus(404);
}
/**
* /!\ Dangerous endpoints
*/
public function testRecoverPhone()
{
$phone = '+33612312312';
$password = Password::factory()->create();
$password->account->generateUserApiKey();
$password->account->activated = false;
$password->account->phone = $phone;
$password->account->save();
config()->set('app.dangerous_endpoints', true);
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => $phone
])->assertJsonValidationErrors(['account_creation_token']);
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => $phone,
'account_creation_token' => 'wrong'
])->assertJsonValidationErrors(['account_creation_token']);
$token = AccountCreationToken::factory()->create();
// Wrong phone
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => '+33612312313', // wrong phone number
'account_creation_token' => $token->token
])->assertJsonValidationErrors(['phone']);
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => $phone,
'account_creation_token' => $token->token
])->assertStatus(200);
$password->account->refresh();
// Use the token a second time
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => $phone,
'account_creation_token' => $token->token
])->assertStatus(422);
$this->get($this->route . '/' . $password->account->identifier . '/recover/' . $password->account->confirmation_key)
->assertStatus(200)
->assertJson([
'activated' => true
]);
$this->get($this->route . '/' . $phone . '/info-by-phone')
->assertStatus(200)
->assertJson([
'activated' => true,
'phone' => true
]);
$this->get($this->route . '/+1234/info-by-phone')
->assertStatus(302);
$this->get($this->route . '/+33612312312/info-by-phone')
->assertStatus(200);
$this->json('GET', $this->route . '/' . $password->account->identifier . '/info-by-phone')
->assertJsonValidationErrors(['phone']);
// Check the mixed username/phone resolution...
$password->account->username = $phone;
$password->account->phone = null;
$password->account->save();
$this->get($this->route . '/' . $phone . '/info-by-phone')
->assertStatus(200)
->assertJson([
'activated' => true,
'phone' => false
]);
$this->assertDatabaseHas('account_creation_tokens', [
'used' => true,
'account_id' => $password->account->id,
]);
}
/**
* /!\ Dangerous endpoints
*/
public function testCreatePublic()
{
$username = 'publicuser';
config()->set('app.dangerous_endpoints', true);
// Missing email
$this->json($this->method, $this->route . '/public', [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
])->assertJsonValidationErrors(['email']);
$this->json($this->method, $this->route . '/public', [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
])->assertJsonValidationErrors(['account_creation_token']);
$token = AccountCreationToken::factory()->create();
$userAgent = 'User Agent Test';
$this->withHeaders([
'User-Agent' => $userAgent,
])->json($this->method, $this->route . '/public', [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
'account_creation_token' => $token->token
])
->assertStatus(200)
->assertJson([
'activated' => false
]);
// Re-use the token
$this->withHeaders([
'User-Agent' => $userAgent,
])->json($this->method, $this->route . '/public', [
'username' => $username . 'foo',
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
'account_creation_token' => $token->token
])->assertStatus(422);
// Already created
$this->json($this->method, $this->route . '/public', [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
])->assertJsonValidationErrors(['username']);
// Email is now unique
config()->set('app.account_email_unique', true);
$this->json($this->method, $this->route . '/public', [
'username' => 'johndoe',
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
])->assertJsonValidationErrors(['email']);
$this->assertDatabaseHas('accounts', [
'username' => $username,
'domain' => config('app.sip_domain'),
'user_agent' => $userAgent
]);
$this->assertDatabaseHas('account_creation_tokens', [
'used' => true,
'account_id' => Account::where('username', $username)->first()->id,
]);
}
public function testCreatePublicPhone()
{
$phone = '+33612312312';
config()->set('app.dangerous_endpoints', true);
// Bad phone format
$this->json($this->method, $this->route . '/public', [
'phone' => 'username',
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
])->assertJsonValidationErrors(['phone']);
$token = AccountCreationToken::factory()->create();
$this->json($this->method, $this->route . '/public', [
'phone' => $phone,
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
'account_creation_token' => $token->token
])
->assertStatus(200)
->assertJson([
'activated' => false
]);
// Already exists
$this->json($this->method, $this->route . '/public', [
'phone' => $phone,
'algorithm' => 'SHA-256',
'password' => '2',
'email' => 'john@doe.tld',
])->assertJsonValidationErrors(['phone']);
$this->assertDatabaseHas('accounts', [
'username' => $phone,
'phone' => $phone,
'domain' => config('app.sip_domain')
]);
}
public function testActivatePhone()
{
$confirmationKey = '0123';
$password = Password::factory()->create();
$password->account->generateUserApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->activated = false;
$password->account->save();
$expiration = new ActivationExpiration();
$expiration->account_id = $password->account->id;
$expiration->expires = Carbon::now()->subYear();
$expiration->save();
$this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200)
->assertJson([
'activated' => false
]);
// Expired
$this->keyAuthenticated($password->account)
->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
'confirmation_key' => $confirmationKey
])
->assertStatus(403);
$expiration->delete();
$this->keyAuthenticated($password->account)
->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
'confirmation_key' => $confirmationKey
])
->assertStatus(200);
$this->assertDatabaseHas('accounts', [
'username' => $password->account->username,
'domain' => $password->account->domain,
'activated' => true
]);
}
public function testChangePassword()
{
$account = Account::factory()->create();
@ -1275,52 +880,6 @@ class ApiAccountTest extends TestCase
]);
}
public function testCodeExpires()
{
$admin = Account::factory()->admin()->create();
$admin->generateUserApiKey();
// Activated, no no confirmation_key
$this->keyAuthenticated($admin)
->json($this->method, $this->route, [
'username' => 'foobar',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => true,
'confirmation_key_expires' => '2040-12-12 12:12:12'
])
->assertStatus(200)
->assertJson([
'confirmation_key_expires' => null
]);
// Bad datetime format
$this->keyAuthenticated($admin)
->json($this->method, $this->route, [
'username' => 'foobar2',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => false,
'confirmation_key_expires' => 'abc'
])
->assertStatus(422);
// Bad datetime format
$this->keyAuthenticated($admin)
->json($this->method, $this->route, [
'username' => 'foobar2',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => false,
'confirmation_key_expires' => '2040-12-12 12:12:12'
])
->assertStatus(200)
->assertJson([
'confirmation_key_expires' => '2040-12-12 12:12:12'
]);
;
}
public function testDelete()
{
$password = Password::factory()->create();