From 75a98c0949447d59cf279a342337bbcff29089fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Tue, 12 Oct 2021 10:56:19 +0200 Subject: [PATCH] Add AccountAction model, CRUD and tests Add AccountType model, CRUD and tests Add AccountContact model, CRUD and tests Allow users to retreive their contacts in JSON and VCARD4.0 format Update the dependencies Complete the documentation Improve the messages displayed when removing accounts in RemoveUnconfirmedAccounts Fix a bug in DeviceController Fix README --- flexiapi/README.md | 2 +- flexiapi/app/Account.php | 117 ++++++++---- flexiapi/app/AccountAction.php | 16 ++ flexiapi/app/AccountType.php | 18 ++ .../Commands/RemoveUnconfirmedAccounts.php | 7 +- flexiapi/app/Contact.php | 11 ++ .../Account/ContactVcardController.php | 46 +++++ .../Controllers/Account/DeviceController.php | 2 +- .../Api/AccountContactController.php | 52 ++++++ .../Api/Admin/AccountActionController.php | 89 +++++++++ .../Api/Admin/AccountContactController.php | 60 ++++++ .../Api/Admin/AccountController.php | 22 +++ .../Api/Admin/AccountTypeController.php | 73 ++++++++ flexiapi/composer.lock | 158 +++++++++++----- flexiapi/composer.phar | Bin 2262733 -> 2268732 bytes ...021_10_13_092937_create_contacts_table.php | 70 +++++++ flexiapi/database/seeds/AccountTypeSeeder.php | 15 ++ flexiapi/database/seeds/DatabaseSeeder.php | 8 +- .../api/documentation_markdown.blade.php | 92 +++++++++- flexiapi/routes/api.php | 27 +++ flexiapi/routes/web.php | 4 + flexiapi/tests/Feature/AccountActionTest.php | 149 +++++++++++++++ .../tests/Feature/AccountContactsTest.php | 138 ++++++++++++++ flexiapi/tests/Feature/AccountTypeTest.php | 171 ++++++++++++++++++ flexisip-account-manager.spec | 2 +- 25 files changed, 1256 insertions(+), 93 deletions(-) create mode 100644 flexiapi/app/AccountAction.php create mode 100644 flexiapi/app/AccountType.php create mode 100644 flexiapi/app/Contact.php create mode 100644 flexiapi/app/Http/Controllers/Account/ContactVcardController.php create mode 100644 flexiapi/app/Http/Controllers/Api/AccountContactController.php create mode 100644 flexiapi/app/Http/Controllers/Api/Admin/AccountActionController.php create mode 100644 flexiapi/app/Http/Controllers/Api/Admin/AccountContactController.php create mode 100644 flexiapi/app/Http/Controllers/Api/Admin/AccountTypeController.php create mode 100644 flexiapi/database/migrations/2021_10_13_092937_create_contacts_table.php create mode 100644 flexiapi/database/seeds/AccountTypeSeeder.php create mode 100644 flexiapi/tests/Feature/AccountActionTest.php create mode 100644 flexiapi/tests/Feature/AccountContactsTest.php create mode 100644 flexiapi/tests/Feature/AccountTypeTest.php diff --git a/flexiapi/README.md b/flexiapi/README.md index ed0f814..29ede77 100644 --- a/flexiapi/README.md +++ b/flexiapi/README.md @@ -116,7 +116,7 @@ If you are planning to send emails using your account manager: ## Usage -For the web panel, a generale documentation is available under the `\documentation` page. +For the web panel, a generale documentation is available under the `/documentation` page. For the REST API, the `/api` page contains all the required documentation to authenticate and request the API. ## Console commands diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index c97af03..2a2dcaa 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -35,8 +35,8 @@ class Account extends Authenticatable { use HasFactory; - protected $with = ['passwords', 'admin', 'emailChanged', 'alias', 'activationExpiration']; - protected $hidden = ['alias', 'expire_time', 'confirmation_key']; + protected $with = ['passwords', 'admin', 'emailChanged', 'alias', 'activationExpiration', 'types', 'actions']; + protected $hidden = ['alias', 'expire_time', 'confirmation_key', 'pivot']; protected $dateTimes = ['creation_time']; protected $appends = ['realm', 'phone', 'confirmation_key_expires']; protected $casts = [ @@ -44,6 +44,9 @@ class Account extends Authenticatable ]; public $timestamps = false; + /** + * Scopes + */ protected static function booted() { static::addGlobalScope('domain', function (Builder $builder) { @@ -63,36 +66,12 @@ class Account extends Authenticatable return $query->where('id', '<', 0); } - public function phoneChangeCode() + /** + * Relations + */ + public function actions() { - return $this->hasOne('App\PhoneChangeCode'); - } - - public function passwords() - { - return $this->hasMany('App\Password'); - } - - public function alias() - { - return $this->hasOne('App\Alias'); - } - - public function nonces() - { - return $this->hasMany('App\DigestNonce'); - } - - public function admin() - { - return $this->hasOne('App\Admin'); - } - - public function hasTombstone() - { - return AccountTombstone::where('username', $this->attributes['username']) - ->where('domain', $this->attributes['domain']) - ->exists(); + return $this->hasMany('App\AccountAction'); } public function activationExpiration() @@ -100,16 +79,54 @@ class Account extends Authenticatable return $this->hasOne('App\ActivationExpiration'); } + public function admin() + { + return $this->hasOne('App\Admin'); + } + + public function alias() + { + return $this->hasOne('App\Alias'); + } + public function apiKey() { return $this->hasOne('App\ApiKey'); } + public function contacts() + { + return $this->belongsToMany('App\Account', 'contacts', 'account_id', 'contact_id'); + } + public function emailChanged() { return $this->hasOne('App\EmailChanged'); } + public function nonces() + { + return $this->hasMany('App\DigestNonce'); + } + + public function passwords() + { + return $this->hasMany('App\Password'); + } + + public function phoneChangeCode() + { + return $this->hasOne('App\PhoneChangeCode'); + } + + public function types() + { + return $this->belongsToMany('App\AccountType'); + } + + /** + * Attributes + */ public function getIdentifierAttribute() { return $this->attributes['username'].'@'.$this->attributes['domain']; @@ -148,6 +165,9 @@ class Account extends Authenticatable return $this->passwords()->where('algorithm', 'SHA-256')->exists(); } + /** + * Utils + */ public function activationExpired(): bool { return ($this->activationExpiration && $this->activationExpiration->isExpired()); @@ -188,6 +208,13 @@ class Account extends Authenticatable return ($this->admin); } + public function hasTombstone() + { + return AccountTombstone::where('username', $this->attributes['username']) + ->where('domain', $this->attributes['domain']) + ->exists(); + } + public function updatePassword($newPassword, $algorithm) { $this->passwords()->delete(); @@ -198,4 +225,32 @@ class Account extends Authenticatable $password->algorithm = $algorithm; $password->save(); } + + public function toVcard4() + { + $vcard = ' +BEGIN:VCARD +VERSION:4.0 +KIND:individual +MEMBER:'.$this->getIdentifierAttribute(); + + if (!empty($this->attributes['display_name'])) { + $vcard . ' +NAME:'.$this->attributes['display_name']; + } + + if ($this->types) { + $vcard .= ' +X-LINPHONE-ACCOUNT-TYPE:'.$this->types->implode('key', ','); + } + + foreach ($this->actions as $action) { + $vcard .= ' +X-LINPHONE-ACCOUNT-ACTION:'.$action->key.';'.$action->code.';'.$action->protocol; + } + + return $vcard . ' +END:VCARD + '; + } } diff --git a/flexiapi/app/AccountAction.php b/flexiapi/app/AccountAction.php new file mode 100644 index 0000000..af1d4ae --- /dev/null +++ b/flexiapi/app/AccountAction.php @@ -0,0 +1,16 @@ +belongsTo('App\Account'); + } +} diff --git a/flexiapi/app/AccountType.php b/flexiapi/app/AccountType.php new file mode 100644 index 0000000..4d9cb82 --- /dev/null +++ b/flexiapi/app/AccountType.php @@ -0,0 +1,18 @@ +belongsToMany('App\Account'); + } +} diff --git a/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php b/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php index 47960bd..b1049ed 100644 --- a/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php +++ b/flexiapi/app/Console/Commands/RemoveUnconfirmedAccounts.php @@ -44,11 +44,14 @@ class RemoveUnconfirmedAccounts extends Command $accounts = $accounts->where('activated', false); } + $count = $accounts->count(); + if ($this->option('apply')) { - $this->info($accounts->count() . ' accounts deleted'); + $this->info($count . ' accounts in deletion…'); $accounts->delete(); + $this->info($count . ' accounts deleted'); } else { - $this->info($accounts->count() . ' accounts to delete'); + $this->info($count . ' accounts to delete'); } } } diff --git a/flexiapi/app/Contact.php b/flexiapi/app/Contact.php new file mode 100644 index 0000000..1909d25 --- /dev/null +++ b/flexiapi/app/Contact.php @@ -0,0 +1,11 @@ +. +*/ + +namespace App\Http\Controllers\Account; + +use Illuminate\Http\Request; + +use App\Http\Controllers\Controller; + +class ContactVcardController extends Controller +{ + public function index(Request $request) + { + return response( + $request->user()->contacts->map(function ($contact) { + return $contact->toVcard4(); + })->implode(' + ') + ); + } + + public function show(Request $request, string $sip) + { + return $request->user() + ->contacts() + ->sip($sip) + ->firstOrFail() + ->toVcard4(); + } +} diff --git a/flexiapi/app/Http/Controllers/Account/DeviceController.php b/flexiapi/app/Http/Controllers/Account/DeviceController.php index 2871f6f..2b79dca 100644 --- a/flexiapi/app/Http/Controllers/Account/DeviceController.php +++ b/flexiapi/app/Http/Controllers/Account/DeviceController.php @@ -46,7 +46,7 @@ class DeviceController extends Controller ]); } - public function destroy(string $uuid) + public function destroy(Request $request, string $uuid) { $connector = new FlexisipConnector; $connector->deleteDevice($request->user()->identifier, $uuid); diff --git a/flexiapi/app/Http/Controllers/Api/AccountContactController.php b/flexiapi/app/Http/Controllers/Api/AccountContactController.php new file mode 100644 index 0000000..520a049 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/AccountContactController.php @@ -0,0 +1,52 @@ +. +*/ + +namespace App\Http\Controllers\Api; + +use Illuminate\Http\Request; + +use App\Http\Controllers\Controller; + +class AccountContactController extends Controller +{ + public function index(Request $request) + { + $contacts = $request->user()->contacts; + + return $request->has('vcard') + ? response($contacts->map(function ($contact) { + return $contact->toVcard4(); + })->implode(' + ') + ) + : $contacts; + } + + public function show(Request $request, string $sip) + { + $contact = $request->user() + ->contacts() + ->sip($sip) + ->firstOrFail(); + + return $request->has('vcard') + ? $contact->toVcard4() + : $contact; + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountActionController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountActionController.php new file mode 100644 index 0000000..d6a2e01 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountActionController.php @@ -0,0 +1,89 @@ +. +*/ + +namespace App\Http\Controllers\Api\Admin; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; + +use App\Account; +use App\AccountAction; +use App\Rules\NoUppercase; + +class AccountActionController extends Controller +{ + public function index(int $id) + { + return Account::findOrFail($id)->actions; + } + + public function get(int $id, int $actionId) + { + return Account::findOrFail($id) + ->actions() + ->where('id', $actionId) + ->firstOrFail(); + } + + public function store(Request $request, int $id) + { + $request->validate([ + 'key' => ['required', 'alpha_dash', new NoUppercase], + 'code' => ['required', 'alpha_num', new NoUppercase], + 'protocol' => 'required|in:sipinfo,rfc2833' + ]); + + $accountAction = new AccountAction; + $accountAction->account_id = Account::findOrFail($id)->id; + $accountAction->key = $request->get('key'); + $accountAction->code = $request->get('code'); + $accountAction->protocol = $request->get('protocol'); + $accountAction->save(); + + return $accountAction; + } + + public function update(Request $request, int $id, int $actionId) + { + $request->validate([ + 'key' => ['alpha_dash', new NoUppercase], + 'code' => ['alpha_num', new NoUppercase], + 'protocol' => 'in:sipinfo,rfc2833' + ]); + + $accountAction = Account::findOrFail($id) + ->actions() + ->where('id', $actionId) + ->firstOrFail(); + $accountAction->key = $request->get('key'); + $accountAction->code = $request->get('code'); + $accountAction->protocol = $request->get('protocol'); + $accountAction->save(); + + return $accountAction; + } + + public function destroy(int $id, int $actionId) + { + return Account::findOrFail($id) + ->actions() + ->where('id', $actionId) + ->delete(); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountContactController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountContactController.php new file mode 100644 index 0000000..9fec022 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountContactController.php @@ -0,0 +1,60 @@ +. +*/ + +namespace App\Http\Controllers\Api\Admin; + +use App\Http\Controllers\Controller; + +use App\Account; + +class AccountContactController extends Controller +{ + public function index(int $id) + { + return Account::findOrFail($id)->contacts; + } + + public function show(int $id, int $contactId) + { + return Account::findOrFail($id) + ->contacts() + ->where('id', $contactId) + ->firstOrFail(); + } + + public function add(int $id, int $contactId) + { + if (Account::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) { + abort(403); + } + + if (Account::findOrFail($contactId)) { + return Account::findOrFail($id)->contacts()->attach($contactId); + } + } + + public function remove(int $id, int $contactId) + { + if (!Account::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) { + abort(403); + } + + return Account::findOrFail($id)->contacts()->detach($contactId); + } +} diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php index fe650e8..c335976 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php @@ -28,6 +28,7 @@ use Carbon\Carbon; use App\Account; use App\AccountTombstone; +use App\AccountType; use App\ActivationExpiration; use App\Admin; use App\Alias; @@ -116,6 +117,7 @@ class AccountController extends Controller $account = new Account; $account->username = $request->get('username'); $account->email = $request->get('email'); + $account->display_name = $request->get('display_name'); $account->activated = $request->has('activated') ? (bool)$request->get('activated') : false; @@ -163,4 +165,24 @@ class AccountController extends Controller return response()->json($account->makeVisible(['confirmation_key'])); } + + public function typeAdd(int $id, int $typeId) + { + if (Account::findOrFail($id)->types()->pluck('id')->contains($typeId)) { + abort(403); + } + + if (AccountType::findOrFail($typeId)) { + return Account::findOrFail($id)->types()->attach($typeId); + } + } + + public function typeRemove(int $id, int $typeId) + { + if (!Account::findOrFail($id)->types()->pluck('id')->contains($typeId)) { + abort(403); + } + + return Account::findOrFail($id)->types()->detach($typeId); + } } diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountTypeController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountTypeController.php new file mode 100644 index 0000000..24f31d9 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountTypeController.php @@ -0,0 +1,73 @@ +. +*/ + +namespace App\Http\Controllers\Api\Admin; + +use App\Http\Controllers\Controller; +use App\Rules\NoUppercase; +use Illuminate\Http\Request; + +use App\AccountType; + +class AccountTypeController extends Controller +{ + public function index() + { + return AccountType::all(); + } + + public function get(int $accountTypeId) + { + return AccountType::where('id', $accountTypeId) + ->firstOrFail(); + } + + public function store(Request $request) + { + $request->validate([ + 'key' => ['required', 'alpha_dash', new NoUppercase], + ]); + + $accountType = new AccountType; + $accountType->key = $request->get('key'); + $accountType->save(); + + return $accountType; + } + + public function update(Request $request, int $accountTypeId) + { + $request->validate([ + 'key' => ['alpha_dash', new NoUppercase], + ]); + + $accountType = AccountType::where('id', $accountTypeId) + ->firstOrFail(); + $accountType->key = $request->get('key'); + $accountType->save(); + + return $accountType; + } + + public function destroy(int $accountTypeId) + { + return AccountType::where('id', $accountTypeId) + ->delete(); + } +} diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index c047123..182c0cd 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -716,16 +716,16 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "84afea85c6841deeea872f36249a206e878a5de0" + "reference": "296c015dc30ec4322168c5ad3ee5cc11dae827ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/84afea85c6841deeea872f36249a206e878a5de0", - "reference": "84afea85c6841deeea872f36249a206e878a5de0", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/296c015dc30ec4322168c5ad3ee5cc11dae827ac", + "reference": "296c015dc30ec4322168c5ad3ee5cc11dae827ac", "shasum": "" }, "require": { @@ -761,7 +761,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.0.2" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.0.3" }, "funding": [ { @@ -773,7 +773,7 @@ "type": "tidelift" } ], - "time": "2021-08-28T21:34:50+00:00" + "time": "2021-10-17T19:48:54+00:00" }, { "name": "guzzlehttp/guzzle", @@ -848,16 +848,16 @@ }, { "name": "guzzlehttp/promises", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/136a635e2b4a49b9d79e9c8fee267ffb257fdba0", + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0", "shasum": "" }, "require": { @@ -869,7 +869,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -885,10 +885,25 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", @@ -897,22 +912,36 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.1" + "source": "https://github.com/guzzle/promises/tree/1.5.0" }, - "time": "2021-03-07T09:25:29+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-07T13:05:22+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.8.2", + "version": "1.8.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", "shasum": "" }, "require": { @@ -949,13 +978,34 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" } ], @@ -972,22 +1022,36 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.2" + "source": "https://github.com/guzzle/psr7/tree/1.8.3" }, - "time": "2021-04-26T09:17:50+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2021-10-05T13:56:00+00:00" }, { "name": "laravel/framework", - "version": "v8.62.0", + "version": "v8.64.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "60a7e00488167ce2babf3a2aeb3677e48aaf39be" + "reference": "3337c029e1bb31d9712d27437cc27010ba302c9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/60a7e00488167ce2babf3a2aeb3677e48aaf39be", - "reference": "60a7e00488167ce2babf3a2aeb3677e48aaf39be", + "url": "https://api.github.com/repos/laravel/framework/zipball/3337c029e1bb31d9712d27437cc27010ba302c9e", + "reference": "3337c029e1bb31d9712d27437cc27010ba302c9e", "shasum": "" }, "require": { @@ -1145,20 +1209,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-09-28T13:30:25+00:00" + "time": "2021-10-12T13:43:13+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.0.2", + "version": "v1.0.3", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "679e24d36ff8b9be0e36f5222244ec8602e18867" + "reference": "6cfc678735f22ccedad761b8cae2bab14c3d8e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/679e24d36ff8b9be0e36f5222244ec8602e18867", - "reference": "679e24d36ff8b9be0e36f5222244ec8602e18867", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/6cfc678735f22ccedad761b8cae2bab14c3d8e5b", + "reference": "6cfc678735f22ccedad761b8cae2bab14c3d8e5b", "shasum": "" }, "require": { @@ -1204,7 +1268,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2021-09-29T13:25:52+00:00" + "time": "2021-10-07T14:00:57+00:00" }, { "name": "laravel/tinker", @@ -2335,16 +2399,16 @@ }, { "name": "psy/psysh", - "version": "v0.10.8", + "version": "v0.10.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "e4573f47750dd6c92dca5aee543fa77513cbd8d3" + "reference": "01281336c4ae557fe4a994544f30d3a1bc204375" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e4573f47750dd6c92dca5aee543fa77513cbd8d3", - "reference": "e4573f47750dd6c92dca5aee543fa77513cbd8d3", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/01281336c4ae557fe4a994544f30d3a1bc204375", + "reference": "01281336c4ae557fe4a994544f30d3a1bc204375", "shasum": "" }, "require": { @@ -2404,9 +2468,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.10.8" + "source": "https://github.com/bobthecow/psysh/tree/v0.10.9" }, - "time": "2021-04-10T16:23:39+00:00" + "time": "2021-10-10T13:37:39+00:00" }, { "name": "ralouphie/getallheaders", @@ -2454,16 +2518,16 @@ }, { "name": "ramsey/collection", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "eaca1dc1054ddd10cbd83c1461907bee6fb528fa" + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/eaca1dc1054ddd10cbd83c1461907bee6fb528fa", - "reference": "eaca1dc1054ddd10cbd83c1461907bee6fb528fa", + "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a", + "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a", "shasum": "" }, "require": { @@ -2517,7 +2581,7 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/1.2.1" + "source": "https://github.com/ramsey/collection/tree/1.2.2" }, "funding": [ { @@ -2529,7 +2593,7 @@ "type": "tidelift" } ], - "time": "2021-08-06T03:41:06+00:00" + "time": "2021-10-10T03:01:02+00:00" }, { "name": "ramsey/uuid", @@ -5445,19 +5509,20 @@ }, { "name": "facade/ignition", - "version": "2.14.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/facade/ignition.git", - "reference": "c6126e291bd44ad3fe482537a145fc70e3320598" + "reference": "3ee6e94815462bcf09bca0efc1c9069685df8da3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/ignition/zipball/c6126e291bd44ad3fe482537a145fc70e3320598", - "reference": "c6126e291bd44ad3fe482537a145fc70e3320598", + "url": "https://api.github.com/repos/facade/ignition/zipball/3ee6e94815462bcf09bca0efc1c9069685df8da3", + "reference": "3ee6e94815462bcf09bca0efc1c9069685df8da3", "shasum": "" }, "require": { + "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "facade/flare-client-php": "^1.9.1", @@ -5517,7 +5582,7 @@ "issues": "https://github.com/facade/ignition/issues", "source": "https://github.com/facade/ignition" }, - "time": "2021-10-01T12:58:45+00:00" + "time": "2021-10-11T15:24:06+00:00" }, { "name": "facade/ignition-contracts", @@ -7642,6 +7707,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index 81380626f4c4780fbc4e1cbf3b98925d45826424..0e7ab8212c170ea286a13f6113154981bc8b7a85 100755 GIT binary patch delta 19311 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 CCZhBJ delta 13674 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 Ic<(F!H>)ou0ssI2 diff --git a/flexiapi/database/migrations/2021_10_13_092937_create_contacts_table.php b/flexiapi/database/migrations/2021_10_13_092937_create_contacts_table.php new file mode 100644 index 0000000..d22c967 --- /dev/null +++ b/flexiapi/database/migrations/2021_10_13_092937_create_contacts_table.php @@ -0,0 +1,70 @@ +string('display_name')->nullable(); + }); + + Schema::create('contacts', function (Blueprint $table) { + $table->integer('account_id')->unsigned(); + $table->integer('contact_id')->unsigned(); + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->unique(['account_id', 'contact_id']); + $table->timestamps(); + }); + + Schema::create('account_types', function (Blueprint $table) { + $table->increments('id'); + $table->string('key'); + $table->timestamps(); + }); + + Schema::create('account_account_type', function (Blueprint $table) { + $table->integer('account_id')->unsigned(); + $table->integer('account_type_id')->unsigned(); + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->foreign('account_type_id')->references('id') + ->on('account_types')->onDelete('cascade'); + $table->unique(['account_id', 'account_type_id']); + $table->timestamps(); + }); + + Schema::create('account_actions', function (Blueprint $table) { + $table->id(); + $table->integer('account_id')->unsigned(); + $table->foreign('account_id')->references('id') + ->on('accounts')->onDelete('cascade'); + $table->string('key'); + $table->string('code'); + $table->string('protocol'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::disableForeignKeyConstraints(); + + Schema::table('accounts', function (Blueprint $table) { + $table->dropColumn('display_name'); + }); + + Schema::dropIfExists('contacts'); + Schema::dropIfExists('account_types'); + Schema::dropIfExists('account_account_type'); + Schema::dropIfExists('account_actions'); + + Schema::enableForeignKeyConstraints(); + } +} diff --git a/flexiapi/database/seeds/AccountTypeSeeder.php b/flexiapi/database/seeds/AccountTypeSeeder.php new file mode 100644 index 0000000..09253fc --- /dev/null +++ b/flexiapi/database/seeds/AccountTypeSeeder.php @@ -0,0 +1,15 @@ + 'phone']); + AccountType::create(['key' => 'door']); + } +} diff --git a/flexiapi/database/seeds/DatabaseSeeder.php b/flexiapi/database/seeds/DatabaseSeeder.php index 91cb6d1..70825fe 100644 --- a/flexiapi/database/seeds/DatabaseSeeder.php +++ b/flexiapi/database/seeds/DatabaseSeeder.php @@ -1,16 +1,12 @@ call(UsersTableSeeder::class); + $this->call(AccountTypeSeeder::class); } } diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index dd176f0..a5b99ff 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -4,7 +4,7 @@ An API to deal with the Flexisip server The API is available under `/api` -A `from` (consisting of the user SIP address, prefixed with `sip:`), `content-type` and `accept` HTTP headers are required to use the API properly +A `from` (consisting of the user SIP address, prefixed with `sip:`), `content-type` and `accept` HTTP headers are REQUIRED to use the API properly ``` > GET /api/{endpoint} @@ -167,6 +167,7 @@ JSON parameters: * `algorithm` required, values can be `SHA-256` or `MD5` * `domain` **not configurable except during test deployments** the value is enforced to the default registration domain set in the global configuration * `activated` optional, a boolean, set to `false` by default +* `display_name` optional, string * `admin` optional, a boolean, set to `false` by default, create an admin account * `phone` optional, a phone number, set a phone number to the account * `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`. @@ -186,6 +187,77 @@ Activate an account. #### `GET /accounts/{id}/deactivate` Deactivate an account. +### Contacts + +#### `GET /accounts/{id}/contacts/` +Get all the account contacts. + +#### `POST /accounts/{id}/contacts/{contact_id}` +Add a contact to the list. + +#### `DELETE /accounts/{id}/contacts/{contact_id}` +Remove a contact from the list. + +### Account Actions + +#### `GET /accounts/{id}/actions/` +Show an account related actions. + +#### `GET /accounts/{id}/actions/{action_id}` +Show an account related action. + +#### `POST /accounts/{id}/actions/` +Create an account action. + +JSON parameters: + +* `key` required, alpha numeric with dashes, lowercase +* `code` required, alpha numeric, lowercase +* `protocol` required, values must be `sipinfo` or `rfc2833` + +#### `PUT /accounts/{id}/actions/{action_id}` +Create an account action. + +JSON parameters: + +* `key` required, alpha numeric with dashes, lowercase +* `code` required, alpha numeric, lowercase +* `protocol` required, values must be `sipinfo` or `rfc2833` + +#### `DELETE /accounts/{id}/actions/{action_id}` +Delete an account related action. + +### Account Types + +#### `GET /account_types/` +Show all the account types. + +#### `GET /account_types/{id}` +Show an account type. + +#### `POST /account_types/` +Create an account type. + +JSON parameters: + +* `key` required, alpha numeric with dashes, lowercase + +#### `PUT /account_types/{id}` +Update an account type. + +JSON parameters: + +* `key` required, alpha numeric with dashes, lowercase + +#### `DELETE /account_types/{id}` +Delete an account type. + +#### `POST /accounts/{id}/types/{type_id}` +Add a type to the account. + +#### `DELETE /accounts/{id}/contacts/{type_id}` +Remove a a type from the account. + ### Statistics #### `GET /statistics/day` @@ -197,12 +269,14 @@ Retrieve registrations statistics for a week. #### `GET /statistics/month` Retrieve registrations statistics for a month. -# Provisioning +# Non-API Endpoints + +The following URLs are **not API endpoints**, they are not located under `/api` but directly under the root path. + +## Provisioning When an account is having an available `confirmation_key` it can be provisioned using the two following URL. -Those two URL are **not API endpoints**, they are not located under `/api`. - ### `VISIT /provisioning/` Return the provisioning information available in the liblinphone configuration file (if correctly configured). @@ -216,4 +290,12 @@ Return a QRCode that points to the provisioning URL. ## Authenticated provisioning ### `VISIT /provisioning/me` -Return the same base content as the previous URL and the account related information, similar to the `confirmation_key` endpoint. However this endpoint will always return those information. \ No newline at end of file +Return the same base content as the previous URL and the account related information, similar to the `confirmation_key` endpoint. However this endpoint will always return those information. + +## Authenticated contact list + +### `VISIT /contacts/vcard` +Return the authenticated user contacts list, in [vCard 4.0 format](https://datatracker.ietf.org/doc/html/rfc6350). + +### `VISIT /contacts/vcard/{sip}` +Return a specific user authenticated contact, in [vCard 4.0 format](https://datatracker.ietf.org/doc/html/rfc6350). \ No newline at end of file diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 0dc3c85..01ba16d 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -49,12 +49,39 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::post('accounts/me/email/request', 'Api\EmailController@requestUpdate'); Route::post('accounts/me/password', 'Api\PasswordController@update'); + Route::get('accounts/me/contacts', 'Api\AccountContactController@index'); + Route::get('accounts/me/contacts/{sip}', 'Api\AccountContactController@show'); + Route::group(['middleware' => ['auth.admin']], function () { + // Accounts Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate'); Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); Route::post('accounts', 'Api\Admin\AccountController@store'); Route::get('accounts', 'Api\Admin\AccountController@index'); Route::get('accounts/{id}', 'Api\Admin\AccountController@show'); Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy'); + + // Account actions + Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index'); + Route::get('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@show'); + Route::post('accounts/{id}/actions', 'Api\Admin\AccountActionController@store'); + Route::delete('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@destroy'); + Route::put('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@update'); + + // Account contacts + Route::get('accounts/{id}/contacts', 'Api\Admin\AccountContactController@index'); + Route::get('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@show'); + Route::post('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@add'); + Route::delete('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@remove'); + + // Account types + Route::get('account_types', 'Api\Admin\AccountTypeController@index'); + Route::get('account_types/{id}', 'Api\Admin\AccountTypeController@show'); + Route::post('account_types', 'Api\Admin\AccountTypeController@store'); + Route::delete('account_types/{id}', 'Api\Admin\AccountTypeController@destroy'); + Route::put('account_types/{id}', 'Api\Admin\AccountTypeController@update'); + + Route::post('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeAdd'); + Route::delete('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeRemove'); }); }); \ No newline at end of file diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index b81daba..8da7081 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -34,6 +34,10 @@ Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@valida Route::group(['middleware' => 'auth.digest_or_key'], function () { Route::get('provisioning/me', 'Account\ProvisioningController@me')->name('provisioning.me'); + + // Vcard 4.0 + Route::get('contacts/vcard/{sip}', 'Account\ContactVcardController@show'); + Route::get('contacts/vcard', 'Account\ContactVcardController@index'); }); Route::get('provisioning/qrcode/{confirmation}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode'); diff --git a/flexiapi/tests/Feature/AccountActionTest.php b/flexiapi/tests/Feature/AccountActionTest.php new file mode 100644 index 0000000..b0bbd9b --- /dev/null +++ b/flexiapi/tests/Feature/AccountActionTest.php @@ -0,0 +1,149 @@ +. +*/ + +namespace Tests\Feature; + +use App\Password; +use App\AccountAction; +use App\Admin; + +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +class AccountActionTest extends TestCase +{ + use RefreshDatabase; + + protected $route = '/api/accounts'; + protected $method = 'POST'; + + public function testCreate() + { + $password = Password::factory()->create(); + + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'key' => '123', + 'code' => '123', + 'protocol' => 'sipinfo' + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountAction::count()); + + // Missing key + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'code' => '123', + 'protocol' => 'sipinfo' + ]) + ->assertStatus(422); + + // Invalid protocol + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'key' => 'abc1234', + 'code' => '123', + 'protocol' => 'wrong' + ]) + ->assertStatus(422); + + // Invalid key + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'key' => 'Abc1234', + 'code' => '123', + 'protocol' => 'wrong' + ]) + ->assertStatus(422); + + $this->keyAuthenticated($admin->account) + ->get($this->route.'/'.$password->account->id.'/actions') + ->assertJson([ + [ + 'key' => '123', + 'code' => '123', + 'protocol' => 'sipinfo' + ] + ]); + } + + public function testDelete() + { + $password = Password::factory()->create(); + + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'key' => '123', + 'code' => '123', + 'protocol' => 'sipinfo' + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountAction::count()); + $accountAction = AccountAction::first(); + + $this->keyAuthenticated($admin->account) + ->delete($this->route.'/'.$password->account->id.'/actions/'.$accountAction->id) + ->assertStatus(200); + + $this->assertEquals(0, AccountAction::count()); + } + + public function testUpdate() + { + $password = Password::factory()->create(); + + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password->account->id.'/actions', [ + 'key' => '123', + 'code' => '123', + 'protocol' => 'sipinfo' + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountAction::count()); + $accountAction = AccountAction::first(); + + $this->keyAuthenticated($admin->account) + ->json('PUT', $this->route.'/'.$password->account->id.'/actions/'.$accountAction->id, [ + 'key' => '123', + 'code' => 'abc', + 'protocol' => 'sipinfo' + ]) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->get($this->route.'/'.$password->account->id.'/actions') + ->assertJson([ + [ + 'code' => 'abc', + ] + ]); + } +} diff --git a/flexiapi/tests/Feature/AccountContactsTest.php b/flexiapi/tests/Feature/AccountContactsTest.php new file mode 100644 index 0000000..9af179e --- /dev/null +++ b/flexiapi/tests/Feature/AccountContactsTest.php @@ -0,0 +1,138 @@ +. +*/ + +namespace Tests\Feature; + +use App\Password; +use App\AccountAction; +use App\AccountType; +use App\Admin; + +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; +use Tests\TestCase; + +class AccountContactTest extends TestCase +{ + use RefreshDatabase; + + protected $route = '/api/accounts'; + protected $method = 'POST'; + + public function testCreate() + { + $password1 = Password::factory()->create(); + $password2 = Password::factory()->create(); + + $typeKey = 'phone'; + $actionKey = '123'; + $actionCode = '123'; + $actionProtocol = 'sipinfo'; + + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password1->account->id.'/contacts/'.$password2->account->id) + ->assertStatus(200); + + $this->assertEquals(1, DB::table('contacts')->count()); + + // Type + $this->keyAuthenticated($admin->account) + ->json($this->method, '/api/account_types', [ + 'key' => $typeKey, + ]) + ->assertStatus(201); + + $accountType = AccountType::first(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, '/api/accounts/'.$password2->account->id.'/types/'.$accountType->id) + ->assertStatus(200); + + // Action + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password2->account->id.'/actions', [ + 'key' => $actionKey, + 'code' => $actionCode, + 'protocol' => $actionProtocol + ]); + + // Retry + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route.'/'.$password1->account->id.'/contacts/'.$password2->account->id) + ->assertStatus(403); + $this->assertEquals(1, DB::table('contacts')->count()); + + $this->keyAuthenticated($admin->account) + ->get($this->route.'/'.$password1->account->id.'/contacts') + ->assertJson([ + [ + 'id' => $password2->account->id + ] + ]); + + // /me + $password1->account->generateApiKey(); + $password1->account->save(); + + $this->keyAuthenticated($password1->account) + ->get($this->route.'/me/contacts') + ->assertStatus(200) + ->assertJson([[ + 'username' => $password2->account->username, + 'activated' => true + ]]); + + // Vcard 4.0 + $this->keyAuthenticated($password1->account) + ->get('/contacts/vcard') + ->assertStatus(200) + ->assertSeeText($typeKey) + ->assertSeeText($actionKey.';'.$actionCode.';'.$actionProtocol); + + $this->keyAuthenticated($password1->account) + ->get('/contacts/vcard/'.$password2->account->identifier) + ->assertStatus(200) + ->assertSeeText($typeKey) + ->assertSeeText($actionKey.';'.$actionCode.';'.$actionProtocol); + + $this->keyAuthenticated($password1->account) + ->get($this->route.'/me/contacts/'.$password2->account->identifier) + ->assertStatus(200) + ->assertJson([ + 'username' => $password2->account->username, + 'activated' => true + ]); + + // Remove + $this->keyAuthenticated($admin->account) + ->delete($this->route.'/'.$password1->account->id.'/contacts/'.$password2->account->id) + ->assertStatus(200); + + $this->assertEquals(0, DB::table('contacts')->count()); + + // Retry + $this->keyAuthenticated($admin->account) + ->delete($this->route.'/'.$password1->account->id.'/contacts/'.$password2->account->id) + ->assertStatus(403); + $this->assertEquals(0, DB::table('contacts')->count()); + } +} diff --git a/flexiapi/tests/Feature/AccountTypeTest.php b/flexiapi/tests/Feature/AccountTypeTest.php new file mode 100644 index 0000000..77fa37d --- /dev/null +++ b/flexiapi/tests/Feature/AccountTypeTest.php @@ -0,0 +1,171 @@ +. +*/ + +namespace Tests\Feature; + +use App\Password; +use App\AccountType; +use App\Admin; + +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; +use Tests\TestCase; + +class AccountTypeTest extends TestCase +{ + use RefreshDatabase; + + protected $route = '/api/account_types'; + protected $method = 'POST'; + + public function testCreate() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, [ + 'key' => 'phone', + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountType::count()); + + // Missing key + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, []) + ->assertStatus(422); + + // Invalid key + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, [ + 'key' => 'Abc1234', + ]) + ->assertStatus(422); + + $this->keyAuthenticated($admin->account) + ->get($this->route) + ->assertJson([ + [ + 'key' => 'phone' + ] + ]); + } + + public function testDelete() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, [ + 'key' => 'phone', + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountType::count()); + $accountType = AccountType::first(); + + $this->keyAuthenticated($admin->account) + ->delete($this->route.'/'.$accountType->id) + ->assertStatus(200); + + $this->assertEquals(0, AccountType::count()); + } + + public function testUpdate() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, [ + 'key' => 'phone', + ]) + ->assertStatus(201); + + $this->assertEquals(1, AccountType::count()); + $accountType = AccountType::first(); + + $this->keyAuthenticated($admin->account) + ->json('PUT', $this->route.'/'.$accountType->id, [ + 'key' => 'door', + ]) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->get($this->route) + ->assertJson([ + [ + 'key' => 'door', + ] + ]); + } + + public function testAccountAddType() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->route, [ + 'key' => 'phone', + ]) + ->assertStatus(201) + ->assertJson([ + 'id' => 1, + 'key' => 'phone', + ]); + + $accountType = AccountType::first(); + $password = Password::factory()->create(); + + $this->keyAuthenticated($admin->account) + ->json($this->method, '/api/accounts/'.$password->account->id.'/types/'.$accountType->id) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->json($this->method, '/api/accounts/'.$password->account->id.'/types/'.$accountType->id) + ->assertStatus(403); + + $this->keyAuthenticated($admin->account) + ->get('/api/accounts/'.$password->account->id) + ->assertJson([ + 'types' => [ + [ + 'id' => $accountType->id, + 'key' => $accountType->key + ] + ] + ]); + + // Remove + $this->keyAuthenticated($admin->account) + ->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id) + ->assertStatus(200); + + $this->assertEquals(0, DB::table('account_account_type')->count()); + + // Retry + $this->keyAuthenticated($admin->account) + ->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id) + ->assertStatus(403); + $this->assertEquals(0, DB::table('account_account_type')->count()); + } +} diff --git a/flexisip-account-manager.spec b/flexisip-account-manager.spec index 7cec5df..bc6b87b 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 107 +%define build_number 108 %define var_dir /var/opt/belledonne-communications %define opt_dir /opt/belledonne-communications/share/flexisip-account-manager