diff --git a/flexiapi/README.md b/flexiapi/README.md index 6680881..08a647c 100644 --- a/flexiapi/README.md +++ b/flexiapi/README.md @@ -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. \ No newline at end of file +Once one account is declared as administrator, you can directly configure the other ones using the web panel. diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 4ba38c8..99e5452 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -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'); diff --git a/flexiapi/app/Http/Controllers/AccountController.php b/flexiapi/app/Http/Controllers/Account/AccountController.php similarity index 95% rename from flexiapi/app/Http/Controllers/AccountController.php rename to flexiapi/app/Http/Controllers/Account/AccountController.php index 522b8c7..fe9504b 100644 --- a/flexiapi/app/Http/Controllers/AccountController.php +++ b/flexiapi/app/Http/Controllers/Account/AccountController.php @@ -17,13 +17,11 @@ along with this program. If not, see . */ -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; diff --git a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php index 1ee0600..359151e 100644 --- a/flexiapi/app/Http/Controllers/Account/AuthenticateController.php +++ b/flexiapi/app/Http/Controllers/Account/AuthenticateController.php @@ -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([ diff --git a/flexiapi/app/Http/Controllers/Api/AccountController.php b/flexiapi/app/Http/Controllers/Api/AccountController.php index b1be576..1943449 100644 --- a/flexiapi/app/Http/Controllers/Api/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/AccountController.php @@ -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(); } } diff --git a/flexiapi/app/Http/Controllers/Api/EmailController.php b/flexiapi/app/Http/Controllers/Api/EmailController.php new file mode 100644 index 0000000..ddf1620 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/EmailController.php @@ -0,0 +1,18 @@ +validate([ + 'email' => ['required', 'email', Rule::notIn([$request->user()->email])], + ]); + $request->user()->requestEmailUpdate($request->get('email')); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/PasswordController.php b/flexiapi/app/Http/Controllers/Api/PasswordController.php new file mode 100644 index 0000000..d9132e8 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/PasswordController.php @@ -0,0 +1,49 @@ +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)); + } + } + } +} diff --git a/flexiapi/resources/views/documentation.blade.php b/flexiapi/resources/views/documentation.blade.php index 94814d4..2e2adb4 100644 --- a/flexiapi/resources/views/documentation.blade.php +++ b/flexiapi/resources/views/documentation.blade.php @@ -48,41 +48,66 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth

Endpoints

+

Ping

+ +

GET /ping

+

Returns pong

+ +

GET /accounts/{sip}/info

+

Retrieve public information about the account.

+

Return 404 if the account doesn't exists.

+ +

POST /accounts/{sip}/activate/email

+

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:

+ + +

POST /accounts/{sip}/activate/phone

+

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:

+ +

Accounts (User)

GET /accounts/me

-

Retrieve the account information.

POST /accounts/email/request

-

Change the account email. An email will be sent to the new email address to confirm the operation.

JSON parameters:

-

POST /accounts/password

-

Change the account password.

JSON parameters:

- -

Accounts (Administrator)

+

Devices

+

GET /devices

+

Return the user registered devices.

+ +

DELETE /devices/{uuid}

+

Remove one of the user registered devices.

+ +

Accounts (Administrator)

Those endpoints are authenticated and requires an admin account.

POST /accounts

-

To create an account directly from the API.

JSON parameters:

-

GET /accounts

-

Retrieve all the accounts, paginated.

GET /accounts/{id}

-

Retrieve a specific account.

DELETE /accounts/{id}

-

Delete a specific account and its related information.

GET /accounts/{id}/activate

-

Activate an account.

GET /accounts/{id}/deactivate

-

Deactivate an account.

-

Ping

- -

GET /ping

- -

Returns pong

- -

Devices

- -

GET /devices

- -

Return the user registered devices.

- -

DELETE /devices/{uuid}

- -

Remove one of the user registered devices.

@endsection diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 12d9286..2101cae 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -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'); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 79ff042..f981c63 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -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'); diff --git a/flexiapi/tests/Feature/AccountApiTest.php b/flexiapi/tests/Feature/AccountApiTest.php index 76df2cf..8376bab 100644 --- a/flexiapi/tests/Feature/AccountApiTest.php +++ b/flexiapi/tests/Feature/AccountApiTest.php @@ -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() diff --git a/flexisip-account-manager.spec b/flexisip-account-manager.spec index 2aff088..8939faa 100644 --- a/flexisip-account-manager.spec +++ b/flexisip-account-manager.spec @@ -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"