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 8138062..0e7ab82 100755 Binary files a/flexiapi/composer.phar and b/flexiapi/composer.phar differ 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