Complete the API with new endpoints

Update the documentation
Bump the build number to 41
This commit is contained in:
Timothée Jaussoin 2021-01-13 14:47:22 +01:00
parent 1290b255c5
commit 70463ae687
12 changed files with 294 additions and 104 deletions

View file

@ -130,4 +130,4 @@ This command will set the admin role to any available FlexiSIP account (the exte
php artisan accounts:set-admin {account_id}
Once one account is declared as administrator, you can directly configure the other ones using the web panel.
Once one account is declared as administrator, you can directly configure the other ones using the web panel.

View file

@ -59,6 +59,18 @@ class Account extends Authenticatable
});
}
public function scopeSip($query, string $sip)
{
if (\str_contains($sip, '@')) {
list($usernane, $domain) = explode('@', $sip);
return $query->where('username', $usernane)
->where('domain', $domain);
};
return $query->where('id', '<', 0);
}
public function passwords()
{
return $this->hasMany('App\Password');

View file

@ -17,13 +17,11 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers;
namespace App\Http\Controllers\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
use App\Account;

View file

@ -25,7 +25,6 @@ use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
use App\Account;
use App\Alias;
@ -35,7 +34,7 @@ use App\Mail\PasswordAuthentication;
class AuthenticateController extends Controller
{
private $emailCodeSize = 13;
public static $emailCodeSize = 13;
public function login(Request $request)
{
@ -49,7 +48,8 @@ class AuthenticateController extends Controller
'password' => 'required'
]);
$account = Account::where('username', $request->get('username'))->first();
$account = Account::where('username', $request->get('username'))
->first();
if (!$account) {
return redirect()->back()->withErrors(['authentication' => 'The account doesn\'t exists']);
@ -89,8 +89,12 @@ class AuthenticateController extends Controller
'g-recaptcha-response' => 'required|captcha',
]);
$account = Account::where('email', $request->get('email'))->first();
$account->confirmation_key = Str::random($this->emailCodeSize);
/**
* Because several accounts can have the same email
*/
$account = Account::where('email', $request->get('email'))
->first();
$account->confirmation_key = Str::random(self::$emailCodeSize);
$account->save();
Mail::to($account)->send(new PasswordAuthentication($account));
@ -100,10 +104,10 @@ class AuthenticateController extends Controller
]);
}
public function authenticateEmailConfirm(Request $request, string $code)
public function validateEmail(Request $request, string $code)
{
$request->merge(['code' => $code]);
$request->validate(['code' => 'required|size:'.$this->emailCodeSize]);
$request->validate(['code' => 'required|size:'.self::$emailCodeSize]);
$account = Account::where('confirmation_key', $code)->firstOrFail();
$account->confirmation_key = null;
@ -111,7 +115,6 @@ class AuthenticateController extends Controller
// If there is already a password set, we directly activate the account
if ($account->passwords()->count() != 0) {
$account->activated = true;
$account->save();
}
$account->save();
@ -140,7 +143,8 @@ class AuthenticateController extends Controller
'g-recaptcha-response' => 'required|captcha',
]);
$account = Account::where('username', $request->get('phone'))->first();
$account = Account::where('username', $request->get('phone'))
->first();
// Try alias
if (!$account) {
@ -173,14 +177,15 @@ class AuthenticateController extends Controller
]);
}
public function authenticatePhoneConfirm(Request $request)
public function validatePhone(Request $request)
{
$request->validate([
'account_id' => 'required',
'code' => 'required|digits:4'
]);
$account = Account::where('id', $request->get('account_id'))->firstOrFail();
$account = Account::where('id', $request->get('account_id'))
->firstOrFail();
if ($account->confirmation_key != $request->get('code')) {
return view('account.login_phone')->withErrors([

View file

@ -20,18 +20,58 @@
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
use App\Http\Controllers\Controller;
use App\Mail\ConfirmedRegistration;
use App\Helpers\Utils;
use App\Account;
use App\Password;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class AccountController extends Controller
{
/**
* Public information on a specific account
*/
public function info(Request $request, string $sip)
{
$account = Account::sip($sip)->firstOrFail();
return \response()->json([
'activated' => $account->activated
]);
}
public function activateEmail(Request $request, string $sip)
{
$request->validate([
'code' => 'required|size:'.WebAuthenticateController::$emailCodeSize
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('code'))
->firstOrFail();
$account->activated = true;
$account->confirmation_key = null;
$account->save();
return $account;
}
public function activatePhone(Request $request, string $sip)
{
$request->validate([
'code' => 'required|digits:4'
]);
$account = Account::sip($sip)
->where('confirmation_key', $request->get('code'))
->firstOrFail();
$account->activated = true;
$account->confirmation_key = null;
$account->save();
return $account;
}
public function show(Request $request)
{
return Account::where('id', $request->user()->id)
@ -39,47 +79,9 @@ class AccountController extends Controller
->first();
}
public function requestEmailUpdate(Request $request)
public function delete(Request $request)
{
$request->validate([
'email' => ['required', 'email', Rule::notIn([$request->user()->email])],
]);
$request->user()->requestEmailUpdate($request->get('email'));
}
public function passwordUpdate(Request $request)
{
$request->validate([
'algorithm' => 'required|in:SHA-256,MD5',
'password' => 'required',
]);
$account = $request->user();
$account->activated = true;
$account->save();
$algorithm = $request->get('algorithm');
if ($account->passwords()->count() > 0) {
$request->validate(['old_password' => 'required']);
foreach ($account->passwords as $password) {
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->domain, $request->get('old_password'), $password->algorithm)
)) {
$account->updatePassword($request->get('password'), $algorithm);
return response()->json();
}
}
return response()->json(['errors' => ['old_password' => 'Incorrect old password']], 422);
} else {
$account->updatePassword($request->get('password'), $algorithm);
if (!empty($account->email)) {
Mail::to($account)->send(new ConfirmedRegistration($account));
}
}
return Account::where('id', $request->user()->id)
->delete();
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class EmailController extends Controller
{
public function requestUpdate(Request $request)
{
$request->validate([
'email' => ['required', 'email', Rule::notIn([$request->user()->email])],
]);
$request->user()->requestEmailUpdate($request->get('email'));
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Helpers\Utils;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller
{
public function update(Request $request)
{
$request->validate([
'algorithm' => 'required|in:SHA-256,MD5',
'password' => 'required',
]);
$account = $request->user();
$account->activated = true;
$account->save();
$algorithm = $request->get('algorithm');
if ($account->passwords()->count() > 0) {
$request->validate(['old_password' => 'required']);
foreach ($account->passwords as $password) {
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->domain, $request->get('old_password'), $password->algorithm)
)) {
$account->updatePassword($request->get('password'), $algorithm);
return response()->json();
}
}
return response()->json(['errors' => ['old_password' => 'Incorrect old password']], 422);
} else {
$account->updatePassword($request->get('password'), $algorithm);
if (!empty($account->email)) {
Mail::to($account)->send(new ConfirmedRegistration($account));
}
}
}
}

View file

@ -48,41 +48,66 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<h2>Endpoints</h2>
<h3>Ping</h3>
<h4><code>GET /ping</code></h4>
<p>Returns <code>pong</code></p>
<h4><code>GET /accounts/{sip}/info</code></h4>
<p>Retrieve public information about the account.</p>
<p>Return <code>404</code> if the account doesn't exists.</p>
<h4><code>POST /accounts/{sip}/activate/email</code></h4>
<p>Activate an account using a secret code received by email.</p>
<p>Return <code>404</code> if the account doesn't exists or if the code is incorrect, the validated account otherwise.</p>
<p>JSON parameters:</p>
<ul>
<li><code>code</code> the code</li>
</ul>
<h4><code>POST /accounts/{sip}/activate/phone</code></h4>
<p>Activate an account using a pin code received by phone.</p>
<p>Return <code>404</code> if the account doesn't exists or if the code is incorrect, the validated account otherwise.</p>
<p>JSON parameters:</p>
<ul>
<li><code>code</code> the PIN code</li>
</ul>
<h3>Accounts (User)</h3>
<h4><code>GET /accounts/me</code></h4>
<p>Retrieve the account information.</p>
<h4><code>POST /accounts/email/request</code></h4>
<p>Change the account email. An email will be sent to the new email address to confirm the operation.</p>
<p>JSON parameters:</p>
<ul>
<li><code>email</code> the new email address</li>
</ul>
<h4><code>POST /accounts/password</code></h4>
<p>Change the account password.</p>
<p>JSON parameters:</p>
<ul>
<li><code>algorithm</code> required, values can be <code>SHA-256</code> or <code>MD5</code></li>
<li><code>old_password</code> required if the password is already set, the old password</li>
<li><code>password</code> required, the new password</li>
</ul>
<h3>Accounts (Administrator)</h3>
<h3>Devices</h3>
<h4><code>GET /devices</code></h4>
<p>Return the user registered devices.</p>
<h4><code>DELETE /devices/{uuid}</code></h4>
<p>Remove one of the user registered devices.</p>
<h3>Accounts (Administrator)</h3>
<p>Those endpoints are authenticated and requires an admin account.</p>
<h4><code>POST /accounts</code></h4>
<p>To create an account directly from the API.</p>
<p>JSON parameters:</p>
<ul>
<li><code>username</code> unique username, minimum 6 characters</li>
<li><code>password</code> required minimum 6 characters</li>
@ -92,38 +117,18 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
</ul>
<h4><code>GET /accounts</code></h4>
<p>Retrieve all the accounts, paginated.</p>
<h4><code>GET /accounts/{id}</code></h4>
<p>Retrieve a specific account.</p>
<h4><code>DELETE /accounts/{id}</code></h4>
<p>Delete a specific account and its related information.</p>
<h4><code>GET /accounts/{id}/activate</code></h4>
<p>Activate an account.</p>
<h4><code>GET /accounts/{id}/deactivate</code></h4>
<p>Deactivate an account.</p>
<h3>Ping</h3>
<h4><code>GET /ping</code></h4>
<p>Returns <code>pong</code></p>
<h3>Devices</h3>
<h4><code>GET /devices</code></h4>
<p>Return the user registered devices.</p>
<h4><code>DELETE /devices/{uuid}</code></h4>
<p>Remove one of the user registered devices.</p>
@endsection

View file

@ -26,14 +26,19 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
});
Route::get('ping', 'Api\PingController@ping');
Route::get('accounts/{sip}/info', 'Api\AccountController@info');
Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmail');
Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone');
Route::group(['middleware' => ['auth.digest_or_key']], function () {
Route::get('accounts/me', 'Api\AccountController@show');
Route::delete('accounts/me', 'Api\AccountController@delete');
Route::get('devices', 'Api\DeviceController@index');
Route::delete('devices/{uuid}', 'Api\DeviceController@destroy');
Route::get('accounts/me', 'Api\AccountController@show');
Route::post('accounts/email/request', 'Api\AccountController@requestEmailUpdate');
Route::post('accounts/password', 'Api\AccountController@passwordUpdate');
Route::post('accounts/email/request', 'Api\EmailController@requestUpdate');
Route::post('accounts/password', 'Api\PasswordController@update');
Route::group(['middleware' => ['auth.admin']], function () {
Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate');

View file

@ -19,20 +19,20 @@
//Route::get('/', 'HomeController@index')->name('home');
Route::get('/', 'AccountController@home')->name('account.home');
Route::get('terms', 'AccountController@terms')->name('account.terms');
Route::get('privacy', 'AccountController@privacy')->name('account.privacy');
Route::get('/', 'Account\AccountController@home')->name('account.home');
Route::get('terms', 'Account\AccountController@terms')->name('account.terms');
Route::get('privacy', 'Account\AccountController@privacy')->name('account.privacy');
Route::get('login', 'Account\AuthenticateController@login')->name('account.login');
Route::post('authenticate', 'Account\AuthenticateController@authenticate')->name('account.authenticate');
Route::get('login/email', 'Account\AuthenticateController@loginEmail')->name('account.login_email');
Route::post('authenticate/email', 'Account\AuthenticateController@authenticateEmail')->name('account.authenticate.email');
Route::get('authenticate/email/{code}', 'Account\AuthenticateController@authenticateEmailConfirm')->name('account.authenticate.email_confirm');
Route::get('authenticate/email/{code}', 'Account\AuthenticateController@validateEmail')->name('account.authenticate.email_confirm');
Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone');
Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone');
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@authenticatePhoneConfirm')->name('account.authenticate.phone_confirm');
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm');
Route::get('register', 'Account\RegisterController@register')->name('account.register');
@ -45,11 +45,11 @@ Route::get('register/email', 'Account\RegisterController@registerEmail')->name('
Route::post('register/email', 'Account\RegisterController@storeEmail')->name('account.store.email');
Route::group(['middleware' => 'auth'], function () {
Route::get('panel', 'AccountController@panel')->name('account.panel');
Route::get('panel', 'Account\AccountController@panel')->name('account.panel');
Route::get('logout', 'Account\AuthenticateController@logout')->name('account.logout');
Route::get('delete', 'AccountController@delete')->name('account.delete');
Route::delete('delete', 'AccountController@destroy')->name('account.destroy');
Route::get('delete', 'Account\AccountController@delete')->name('account.delete');
Route::delete('delete', 'Account\AccountController@destroy')->name('account.destroy');
Route::get('email', 'Account\EmailController@show')->name('account.email');
Route::post('email/request', 'Account\EmailController@requestUpdate')->name('account.email.request_update');

View file

@ -184,6 +184,18 @@ class AccountApiTest extends TestCase
$password = Password::factory()->create();
$password->account->generateApiKey();
/**
* Public information
*/
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => false
]);
/**
* Retrieve the authenticated account
*/
$this->keyAuthenticated($password->account)
->get($this->route.'/me')
->assertStatus(200)
@ -191,6 +203,91 @@ class AccountApiTest extends TestCase
'username' => $password->account->username,
'activated' => false
]);
/**
* Retrieve the authenticated account
*/
$this->keyAuthenticated($password->account)
->delete($this->route.'/me')
->assertStatus(200);
/**
* Check again
*/
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(404);
}
public function testActivateEmail()
{
$confirmationKey = '0123456789abc';
$password = Password::factory()->create();
$password->account->generateApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->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', [
'code' => $confirmationKey
])
->assertStatus(404);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
'code' => $confirmationKey.'longer'
])
->assertStatus(422);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
'code' => 'X123456789abc'
])
->assertStatus(404);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
'code' => $confirmationKey
])
->assertStatus(200);
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => true
]);
}
public function testActivatePhone()
{
$confirmationKey = '0123';
$password = Password::factory()->create();
$password->account->generateApiKey();
$password->account->confirmation_key = $confirmationKey;
$password->account->save();
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => false
]);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [
'code' => $confirmationKey
])
->assertStatus(200);
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => true
]);
}
public function testChangeEmail()
@ -373,7 +470,6 @@ class AccountApiTest extends TestCase
->assertJson([
'id' => 1
]);
}
public function testDelete()

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 40
%define build_number 41
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager
%define env_file "$RPM_BUILD_ROOT/etc/flexisip-account-manager/flexiapi.env"