Fix FLEXIAPI-359 Add CardDav servers support in the spaces

This commit is contained in:
Timothée Jaussoin 2025-08-26 09:03:50 +00:00
parent 9a9b8ab34e
commit 60df61d508
103 changed files with 2453 additions and 1137 deletions

View file

@ -4,6 +4,7 @@ v2.1
---- ----
- Fix FLEXIAPI-282 Migrate to Laravel 11 and PHP 8.2+ - Fix FLEXIAPI-282 Migrate to Laravel 11 and PHP 8.2+
- Fix FLEXIAPI-371 Add documentation for the Wizard page - Fix FLEXIAPI-371 Add documentation for the Wizard page
- Fix FLEXIAPI-359 Add CardDav servers support in the spaces
v2.0 v2.0
---- ----

View file

@ -25,6 +25,7 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Collection;
use Carbon\Carbon; use Carbon\Carbon;
use Awobaz\Compoships\Compoships; use Awobaz\Compoships\Compoships;
@ -36,7 +37,7 @@ class Account extends Authenticatable
use HasFactory; use HasFactory;
use Compoships; use Compoships;
protected $with = ['passwords', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries']; protected $with = ['passwords', 'emailChangeCode', 'types', 'actions', 'dictionaryEntries', 'carddavServers'];
protected $hidden = ['expire_time', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries']; protected $hidden = ['expire_time', 'pivot', 'currentProvisioningToken', 'currentRecoveryCode', 'dictionaryEntries'];
protected $appends = ['realm', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary']; protected $appends = ['realm', 'provisioning_token', 'provisioning_token_expire_at', 'dictionary'];
protected $casts = [ protected $casts = [
@ -149,6 +150,12 @@ class Account extends Authenticatable
return $this->hasMany(AccountDictionaryEntry::class); return $this->hasMany(AccountDictionaryEntry::class);
} }
public function carddavServers()
{
return $this->belongsToMany(SpaceCardDavServer::class, 'account_carddav_credentials', 'account_id', 'space_carddav_server_id')
->withPivot('username', 'domain', 'algorithm', 'password');
}
public function getDictionaryAttribute() public function getDictionaryAttribute()
{ {
if ($this->dictionaryEntries->isEmpty()) return new stdClass; if ($this->dictionaryEntries->isEmpty()) return new stdClass;
@ -309,6 +316,15 @@ class Account extends Authenticatable
return null; return null;
} }
public function getRemainingCardDavCredentialsCreatableAttribute(): Collection
{
return $this->space->carddavServers()->whereNotIn('id', function ($query) {
$query->select('space_carddav_server_id')
->from('account_carddav_credentials')
->where('account_id', $this->id);
})->get();
}
public function getIdentifierAttribute(): string public function getIdentifierAttribute(): string
{ {
return $this->attributes['username'] . '@' . $this->attributes['domain']; return $this->attributes['username'] . '@' . $this->attributes['domain'];

View file

@ -0,0 +1,20 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AccountCardDavCredentials extends Model
{
protected $table = 'account_carddav_credentials';
public function cardDavServer()
{
return $this->hasOne(SpaceCardDavServer::class, 'id', 'space_carddav_server_id');
}
public function getIdentifierAttribute()
{
return $this->username . '@' . $this->domain;
}
}

View file

@ -28,11 +28,9 @@ use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension; use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
$hostSpace = null;
function space(): ?Space function space(): ?Space
{ {
return request()->space; return is_object(request()->space) ? request()->space :null;
} }
function passwordAlgorithms(): array function passwordAlgorithms(): array

View file

@ -189,6 +189,44 @@ class ProvisioningController extends Controller
} }
} }
$remoteContactDirectoryCounter = 0;
$authInfoIndex = 0;
// CardDav servers
if ($request->space?->carddavServers) {
foreach ($request->space->carddavServers as $carddavServer) {
$carddavServer->getProvisioningSection($config, $remoteContactDirectoryCounter);
$remoteContactDirectoryCounter++;
}
}
if ($account) {
foreach ($account->carddavServers as $carddavServer) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$config->appendChild($section);
$entry = $dom->createElement('entry', $carddavServer->pivot->username);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->domain);
$entry->setAttribute('name', 'domain');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->password);
$entry->setAttribute('name', 'ha1');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $carddavServer->pivot->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
$authInfoIndex++;
}
}
// Password reset // Password reset
if ($account && $request->has('reset_password')) { if ($account && $request->has('reset_password')) {
$account->updatePassword(Str::random(10)); $account->updatePassword(Str::random(10));

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Facades\Log;
use App\Account; use App\Account;
use App\AccountType; use App\AccountType;
class AccountAccountTypeController extends Controller class AccountTypeController extends Controller
{ {
public function create(int $id) public function create(int $id)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -27,7 +27,7 @@ use App\Account;
use App\AccountAction; use App\AccountAction;
use App\Rules\NoUppercase; use App\Rules\NoUppercase;
class AccountActionController extends Controller class ActionController extends Controller
{ {
public function create(int $accountId) public function create(int $accountId)
{ {

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Account; use App\Account;
class AccountActivityController extends Controller class ActivityController extends Controller
{ {
public function index(int $accountId) public function index(int $accountId)
{ {

View file

@ -0,0 +1,104 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Controllers\Admin\Account;
use App\Account;
use App\AccountCardDavCredentials;
use App\Http\Controllers\Controller;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use App\Http\Requests\Account\CardDavCredentials;
class CardDavCredentialsController extends Controller
{
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
return view('admin.account.carddav.create', [
'account' => $account,
'carddavServers' => $account->remainingCardDavCredentialsCreatable
]);
}
public function store(CardDavCredentials $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$request->validate([
'carddav_id' => ['required', Rule::exists('space_carddav_servers', 'id')->where(function (Builder $query) use ($account) {
return $query->where('space_id', $account->space->id);
})]
]);
$accountCarddavCredentials = new AccountCardDavCredentials;
$accountCarddavCredentials->space_carddav_server_id = $request->get('carddav_id');
$accountCarddavCredentials->account_id = $account->id;
$accountCarddavCredentials->username = $request->get('username');
$accountCarddavCredentials->domain = $request->get('domain');
$accountCarddavCredentials->password = bchash(
$request->get('username'),
$request->get('domain'),
$request->get('password'),
$request->get('algorithm')
);
$accountCarddavCredentials->algorithm = $request->get('algorithm');
$accountCarddavCredentials->save();
return redirect()->route('admin.account.show', $account);
}
public function delete(int $accountId, int $cardDavId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$accountCarddavCredentials = AccountCardDavCredentials::where('space_carddav_server_id', $cardDavId)
->where('account_id', $account->id)
->firstOrFail();
return view('admin.account.carddav.delete', [
'account' => $account,
'carddavCredentials' => $accountCarddavCredentials,
]);
}
public function destroy(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$this->checkFeatureEnabled($account);
$accountCarddavCredentials = AccountCardDavCredentials::where('space_carddav_server_id', $request->carddav_id)
->where('account_id', $account->id)
->delete();
return redirect()->route('admin.account.show', $account);
}
private function checkFeatureEnabled(Account $account)
{
if (!$account->space->carddav_user_credentials) {
abort(403, 'CardDav Credentials features disabled');
}
}
}

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Facades\Log;
use App\Account; use App\Account;
use App\ContactsList; use App\ContactsList;
class AccountContactController extends Controller class ContactController extends Controller
{ {
public function index(int $accountId) public function index(int $accountId)
{ {

View file

@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Account; use App\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Libraries\FlexisipRedisConnector; use App\Libraries\FlexisipRedisConnector;
class AccountDeviceController extends Controller class DeviceController extends Controller
{ {
public function index(int $accountId) public function index(int $accountId)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -25,7 +25,7 @@ use Illuminate\Http\Request;
use App\Account; use App\Account;
use App\AccountDictionaryEntry; use App\AccountDictionaryEntry;
class AccountDictionaryController extends Controller class DictionaryController extends Controller
{ {
public function create(int $accountId) public function create(int $accountId)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Account; use App\Account;
use App\ExternalAccount; use App\ExternalAccount;
@ -31,7 +31,7 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File; use Illuminate\Validation\Rules\File;
use Propaganistas\LaravelPhone\PhoneNumber; use Propaganistas\LaravelPhone\PhoneNumber;
class AccountImportController extends Controller class ImportController extends Controller
{ {
private Collection $errors; private Collection $errors;
private string $importDirectory = 'imported_csv'; private string $importDirectory = 'imported_csv';

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Account; use App\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -26,7 +26,7 @@ use App\StatisticsCall;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class AccountStatisticsController extends Controller class StatisticsController extends Controller
{ {
public function edit(Request $request, int $accountId) public function edit(Request $request, int $accountId)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -27,7 +27,7 @@ use Illuminate\Validation\Rule;
use App\AccountType; use App\AccountType;
use App\Rules\NoUppercase; use App\Rules\NoUppercase;
class AccountTypeController extends Controller class TypeController extends Controller
{ {
public function index() public function index()
{ {

View file

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\CardDavServer;
use Illuminate\Http\Request;
use App\Space;
use App\SpaceCardDavServer;
class CardDavServerController extends Controller
{
public function create(Space $space)
{
return view('admin.space.carddav_server.create_edit', [
'space' => $space,
'carddavServer' => new SpaceCardDavServer
]);
}
public function store(CardDavServer $request)
{
$carddavServer = new SpaceCardDavServer;
$carddavServer->space_id = $request->space->id;
$carddavServer->fill($request->validated());
$carddavServer->enabled = getRequestBoolean($request, 'enabled');
$carddavServer->use_exact_match_policy = getRequestBoolean($request, 'use_exact_match_policy');
$carddavServer->save();
return redirect()->route('admin.spaces.integration', $request->space);
}
public function edit(Space $space, int $carddavServerId)
{
return view('admin.space.carddav_server.create_edit', [
'space' => $space,
'carddavServer' => $space->carddavServers()->findOrFail($carddavServerId)
]);
}
public function update(CardDavServer $request, Space $space, int $carddavServerId)
{
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->fill($request->validated());
$carddavServer->enabled = getRequestBoolean($request, 'enabled');
$carddavServer->use_exact_match_policy = getRequestBoolean($request, 'use_exact_match_policy');
$carddavServer->save();
return redirect()->route('admin.spaces.integration', $request->space);
}
public function delete(Space $space, int $carddavServerId)
{
return view('admin.space.carddav_server.delete', [
'space' => $space,
'carddavServer' => $space->carddavServers()->findOrFail($carddavServerId)
]);
}
public function destroy(Space $space, int $carddavServerId)
{
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->delete();
return redirect()->route('admin.spaces.integration', $space->id);
}
}

View file

@ -146,6 +146,7 @@ class SpaceController extends Controller
$space->expire_at = $request->get('expire_at'); $space->expire_at = $request->get('expire_at');
$space->custom_theme = getRequestBoolean($request, 'custom_theme'); $space->custom_theme = getRequestBoolean($request, 'custom_theme');
$space->web_panel = getRequestBoolean($request, 'web_panel'); $space->web_panel = getRequestBoolean($request, 'web_panel');
$space->carddav_user_credentials = getRequestBoolean($request, 'carddav_user_credentials');
$space->save(); $space->save();
return redirect()->route('admin.spaces.show', $space); return redirect()->route('admin.spaces.show', $space);

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use App\Account;
use App\AccountAction; use App\AccountAction;
use App\Rules\NoUppercase; use App\Rules\NoUppercase;
class AccountActionController extends Controller class ActionController extends Controller
{ {
public function index(int $id) public function index(int $id)
{ {

View file

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Account;
use App\Space;
use App\AccountCardDavCredentials;
use App\SpaceCardDavServer;
use App\Http\Requests\Account\CardDavCredentials;
class CardDavCredentialsController extends Controller
{
public function index(Request $request, int $accountId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServers = $account->carddavServers;
if ($cardDavServers->isEmpty()) return new \stdClass;
return $cardDavServers->map(function ($cardDavServer) {
return $this->extractCardDavServer($cardDavServer);
})->keyBy('carddav_id');
}
public function show(Request $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $account->cardDavServers()->findOrFail($cardDavServerId);
return $this->extractCardDavServer($cardDavServer);
}
public function update(CardDavCredentials $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $request->space->cardDavServers()->findOrFail($cardDavServerId);
$accountCarddavCredentials = AccountCardDavCredentials::where('account_id', $account->id)
->where('space_carddav_server_id', $cardDavServer->id)
->delete();
$accountCarddavCredentials = new AccountCardDavCredentials;
$accountCarddavCredentials->space_carddav_server_id = $cardDavServer->id;
$accountCarddavCredentials->account_id = $account->id;
$accountCarddavCredentials->username = $request->get('username');
$accountCarddavCredentials->domain = $request->get('domain');
$accountCarddavCredentials->password = bchash(
$request->get('username'),
$request->get('domain'),
$request->get('password'),
$request->get('algorithm')
);
$accountCarddavCredentials->algorithm = $request->get('algorithm');
return $accountCarddavCredentials->save();
}
public function destroy(Request $request, int $accountId, int $cardDavServerId)
{
$account = $request->space->accounts()->findOrFail($accountId);
$cardDavServer = $account->cardDavServers()->findOrFail($cardDavServerId);
return $cardDavServer->delete();
}
private function extractCardDavServer(SpaceCardDavServer $cardDavServer)
{
return [
'carddav_id' => $cardDavServer->id,
'username' => $cardDavServer->pivot->username,
'domain' => $cardDavServer->pivot->domain,
'algorithm' => $cardDavServer->pivot->algorithm,
'password' => $cardDavServer->pivot->password,
];
}
}

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Account; use App\Account;
class AccountContactController extends Controller class ContactController extends Controller
{ {
public function index(int $id) public function index(int $id)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -26,7 +26,7 @@ use Illuminate\Support\Str;
use App\AccountCreationToken; use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class AccountCreationTokenController extends Controller class CreationTokenController extends Controller
{ {
public function create(Request $request) public function create(Request $request)
{ {

View file

@ -17,14 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Account; use App\Account;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class AccountDictionaryController extends Controller class DictionaryController extends Controller
{ {
public function index(int $accountId) public function index(int $accountId)
{ {

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Rules\NoUppercase; use App\Rules\NoUppercase;
@ -25,7 +25,7 @@ use Illuminate\Http\Request;
use App\AccountType; use App\AccountType;
class AccountTypeController extends Controller class TypeController extends Controller
{ {
public function index() public function index()
{ {

View file

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\Admin\Space;
use App\Http\Controllers\Controller;
use App\Http\Requests\Space\CardDavServer;
use Illuminate\Http\Request;
use App\Space;
use App\SpaceCardDavServer;
class CardDavServerController extends Controller
{
public function index(string $host)
{
return Space::where('host', $host)->firstOrFail()->carddavServers;
}
public function show(string $host, int $carddavServerId)
{
return Space::where('host', $host)->firstOrFail()->carddavServers()->findOrFail($carddavServerId);
}
public function store(CardDavServer $request, string $host)
{
$space = Space::where('host', $host)->firstOrFail();
$carddavServer = new SpaceCardDavServer;
$carddavServer->space_id = $space->id;
$carddavServer->fill($request->validated());
$carddavServer->enabled = $request->has('enabled') && (bool)$request->get('enabled');
$carddavServer->use_exact_match_policy = $request->has('use_exact_match_policy') && (bool)$request->get('use_exact_match_policy');
return $carddavServer->save();
}
public function update(CardDavServer $request, string $host, int $carddavServerId)
{
$space = Space::where('host', $host)->firstOrFail();
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
$carddavServer->fill($request->validated());
$carddavServer->enabled = $request->has('enabled') && (bool)$request->get('enabled');
$carddavServer->use_exact_match_policy = $request->has('use_exact_match_policy') && (bool)$request->get('use_exact_match_policy');
return $carddavServer->save();
}
public function destroy(string $host, int $carddavServerId)
{
$space = Space::where('host', $host)->firstOrFail();
$carddavServer = $space->carddavServers()->findOrFail($carddavServerId);
return $carddavServer->delete();
}
}

View file

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin\Space;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\EmailServer\CreateUpdate; use App\Http\Requests\EmailServer\CreateUpdate;

View file

@ -45,38 +45,37 @@ class SpaceController extends Controller
]); ]);
$space = new Space; $space = new Space;
$space->name = $request->get('name');
$space->domain = $request->get('domain');
$space->host = $request->get('host');
$this->setRequestBoolean($request, $space, 'super');
$this->setRequestBoolean($request, $space, 'disable_chat_feature');
$this->setRequestBoolean($request, $space, 'disable_meetings_feature');
$this->setRequestBoolean($request, $space, 'disable_broadcast_feature');
$this->setRequestBoolean($request, $space, 'hide_settings');
$this->setRequestBoolean($request, $space, 'hide_account_settings');
$this->setRequestBoolean($request, $space, 'disable_call_recordings_feature');
$this->setRequestBoolean($request, $space, 'only_display_sip_uri_username');
$this->setRequestBoolean($request, $space, 'assistant_hide_create_account');
$this->setRequestBoolean($request, $space, 'assistant_disable_qr_code');
$this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->expire_at = $request->get('expire_at');
$space->copyright_text = $request->get('copyright_text');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address'); $space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm'); $space->account_realm = $request->get('account_realm');
$space->copyright_text = $request->get('copyright_text');
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries'); $space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
$space->domain = $request->get('domain');
$space->expire_at = $request->get('expire_at');
$space->host = $request->get('host');
$space->intro_registration_text = $request->get('intro_registration_text');
$space->max_account = $request->get('max_account', 0);
$space->max_accounts = $request->get('max_accounts', 0);
$space->name = $request->get('name');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$this->setRequestBoolean($request, $space, 'assistant_disable_qr_code');
$this->setRequestBoolean($request, $space, 'assistant_hide_create_account');
$this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account');
$this->setRequestBoolean($request, $space, 'carddav_user_credentials');
$this->setRequestBoolean($request, $space, 'custom_provisioning_overwrite_all'); $this->setRequestBoolean($request, $space, 'custom_provisioning_overwrite_all');
$this->setRequestBoolean($request, $space, 'provisioning_use_linphone_provisioning_header');
$this->setRequestBoolean($request, $space, 'custom_theme'); $this->setRequestBoolean($request, $space, 'custom_theme');
$this->setRequestBoolean($request, $space, 'web_panel'); $this->setRequestBoolean($request, $space, 'disable_broadcast_feature');
$this->setRequestBoolean($request, $space, 'public_registration'); $this->setRequestBoolean($request, $space, 'disable_call_recordings_feature');
$this->setRequestBoolean($request, $space, 'phone_registration'); $this->setRequestBoolean($request, $space, 'disable_chat_feature');
$this->setRequestBoolean($request, $space, 'disable_meetings_feature');
$this->setRequestBoolean($request, $space, 'hide_account_settings');
$this->setRequestBoolean($request, $space, 'hide_settings');
$this->setRequestBoolean($request, $space, 'intercom_features'); $this->setRequestBoolean($request, $space, 'intercom_features');
$this->setRequestBoolean($request, $space, 'only_display_sip_uri_username');
$this->setRequestBoolean($request, $space, 'phone_registration');
$this->setRequestBoolean($request, $space, 'provisioning_use_linphone_provisioning_header');
$this->setRequestBoolean($request, $space, 'public_registration');
$this->setRequestBoolean($request, $space, 'super');
$this->setRequestBoolean($request, $space, 'web_panel');
$space->save(); $space->save();
return $space->refresh(); return $space->refresh();
@ -90,30 +89,30 @@ class SpaceController extends Controller
public function update(Request $request, string $domain) public function update(Request $request, string $domain)
{ {
$request->validate([ $request->validate([
'super' => 'required|boolean',
'disable_chat_feature' => 'required|boolean',
'disable_meetings_feature' => 'required|boolean',
'disable_broadcast_feature' => 'required|boolean',
'hide_settings' => 'required|boolean',
'hide_account_settings' => 'required|boolean',
'disable_call_recordings_feature' => 'required|boolean',
'only_display_sip_uri_username' => 'required|boolean',
'assistant_hide_create_account' => 'required|boolean',
'assistant_disable_qr_code' => 'required|boolean',
'assistant_hide_third_party_account' => 'required|boolean',
'max_account' => 'required|integer',
'max_accounts' => 'required|integer',
'expire_at' => 'nullable|date|after_or_equal:today',
'account_realm' => ['nullable', new Domain()], 'account_realm' => ['nullable', new Domain()],
'assistant_disable_qr_code' => 'required|boolean',
'assistant_hide_create_account' => 'required|boolean',
'assistant_hide_third_party_account' => 'required|boolean',
'carddav_user_credentials' => 'required|boolean',
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)], 'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
'custom_provisioning_overwrite_all' => 'required|boolean', 'custom_provisioning_overwrite_all' => 'required|boolean',
'provisioning_use_linphone_provisioning_header' => 'required|boolean',
'custom_theme' => 'required|boolean', 'custom_theme' => 'required|boolean',
'web_panel' => 'required|boolean', 'disable_broadcast_feature' => 'required|boolean',
'public_registration' => 'required|boolean', 'disable_call_recordings_feature' => 'required|boolean',
'phone_registration' => 'required|boolean', 'disable_chat_feature' => 'required|boolean',
'disable_meetings_feature' => 'required|boolean',
'expire_at' => 'nullable|date|after_or_equal:today',
'hide_account_settings' => 'required|boolean',
'hide_settings' => 'required|boolean',
'intercom_features' => 'required|boolean', 'intercom_features' => 'required|boolean',
'max_account' => 'required|integer',
'max_accounts' => 'required|integer',
'only_display_sip_uri_username' => 'required|boolean',
'phone_registration' => 'required|boolean',
'provisioning_use_linphone_provisioning_header' => 'required|boolean',
'public_registration' => 'required|boolean',
'super' => 'required|boolean',
'web_panel' => 'required|boolean',
]); ]);
$space = Space::where('domain', $domain)->firstOrFail(); $space = Space::where('domain', $domain)->firstOrFail();

View file

@ -72,30 +72,31 @@ class Kernel extends HttpKernel
* @var array * @var array
*/ */
protected $middlewareAliases = [ protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class, 'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class,
'auth.super_admin' => \App\Http\Middleware\AuthenticateSuperAdmin::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.check_blocked' => \App\Http\Middleware\CheckBlocked::class,
'auth.digest_or_key' => \App\Http\Middleware\AuthenticateDigestOrKey::class, 'auth.digest_or_key' => \App\Http\Middleware\AuthenticateDigestOrKey::class,
'auth.jwt' => \App\Http\Middleware\AuthenticateJWT::class, 'auth.jwt' => \App\Http\Middleware\AuthenticateJWT::class,
'auth.check_blocked' => \App\Http\Middleware\CheckBlocked::class, 'auth.super_admin' => \App\Http\Middleware\AuthenticateSuperAdmin::class,
'validate_json' => \App\Http\Middleware\ValidateJSON::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'web_panel_enabled' => \App\Http\Middleware\IsWebPanelEnabled::class,
'public_registration' => \App\Http\Middleware\IsPublicRegistration::class,
'phone_registration' => \App\Http\Middleware\IsPhoneRegistration::class,
'intercom_features' => \App\Http\Middleware\IsIntercomFeatures::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'cookie' => \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
'cookie.encrypt' => \App\Http\Middleware\EncryptCookies::class, 'cookie.encrypt' => \App\Http\Middleware\EncryptCookies::class,
'cookie' => \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
'feature.carddav_user_credentials' => \App\Http\Middleware\IsCardDavCredentialsEnabled::class,
'feature.intercom' => \App\Http\Middleware\IsIntercomFeatures::class,
'feature.phone_registration' => \App\Http\Middleware\IsPhoneRegistration::class,
'feature.public_registration' => \App\Http\Middleware\IsPublicRegistration::class,
'feature.web_panel_enabled' => \App\Http\Middleware\IsWebPanelEnabled::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'localization' => \App\Http\Middleware\Localization::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'space.check' => \App\Http\Middleware\SpaceCheck::class, 'space.check' => \App\Http\Middleware\SpaceCheck::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'validate_json' => \App\Http\Middleware\ValidateJSON::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'localization' => \App\Http\Middleware\Localization::class,
]; ];
/** /**

View file

@ -29,7 +29,7 @@ class AuthenticateAdmin
return redirect()->route('account.login'); return redirect()->route('account.login');
} }
if (!$request->user()->admin) { if (!$request->user()->admin || $request->user()->domain != $request->space->domain) {
return abort(403, 'Unauthorized area'); return abort(403, 'Unauthorized area');
} }

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsCardDavCredentialsEnabled
{
public function handle(Request $request, Closure $next): Response
{
if ($request->space->carddav_user_credentials) {
return $next($request);
}
return abort(403, 'CardDav Credentials features disabled');
}
}

View file

@ -0,0 +1,39 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Account;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Rules\Domain;
use App\Rules\PasswordAlgorithm;
class CardDavCredentials extends FormRequest
{
public function rules()
{
return [
'username' => 'required|min:3',
'password' => 'required',
'algorithm' => ['required', new PasswordAlgorithm],
'domain' => ['required', new Domain],
];
}
}

View file

@ -0,0 +1,42 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2023 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Http\Requests\Space;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\EmailServer;
use App\Rules\CommaList;
class CardDavServer extends FormRequest
{
public function rules()
{
return [
'uri' => 'required|url',
'min_characters' => 'integer|min:1',
'results_limit' => 'integer|min:0',
'timeout' => 'integer|min:1',
'delay' => 'integer|min:100',
'fields_for_user_input' => [new CommaList],
'fields_for_domain' => [new CommaList],
];
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class CommaList implements Rule
{
public function passes($attribute, $value)
{
preg_match('/^(\w+),(\w)*/', $value, $matches);
return $value == null || (!empty($matches) && $matches[0] == $value);
}
public function message()
{
return 'The :attribute should be null or contain a list of words separated by commas';
}
}

View file

@ -23,11 +23,6 @@ use Illuminate\Contracts\Validation\Rule;
class WithoutSpaces implements Rule class WithoutSpaces implements Rule
{ {
public function __construct()
{
//
}
public function passes($attribute, $value) public function passes($attribute, $value)
{ {
return preg_match('/^\S*$/u', $value); return preg_match('/^\S*$/u', $value);

View file

@ -21,52 +21,67 @@ namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon; use Carbon\Carbon;
class Space extends Model class Space extends Model
{ {
use HasFactory; use HasFactory;
protected $with = ['emailServer']; protected $with = ['emailServer', 'carddavServers'];
public const FORBIDDEN_KEYS = [ public const FORBIDDEN_KEYS = [
'disable_chat_feature', 'account_proxy_registrar_address',
'disable_meetings_feature', 'account_realm',
'disable_broadcast_feature',
'max_account',
'hide_settings',
'hide_account_settings',
'disable_call_recordings_feature',
'only_display_sip_uri_username',
'assistant_hide_create_account',
'assistant_disable_qr_code', 'assistant_disable_qr_code',
'assistant_hide_create_account',
'assistant_hide_third_party_account', 'assistant_hide_third_party_account',
'copyright_text', 'copyright_text',
'disable_broadcast_feature',
'disable_call_recordings_feature',
'disable_chat_feature',
'disable_meetings_feature',
'hide_account_settings',
'hide_settings',
'intro_registration_text', 'intro_registration_text',
'max_account',
'newsletter_registration_address', 'newsletter_registration_address',
'account_proxy_registrar_address', 'only_display_sip_uri_username',
'account_realm'
]; ];
protected $hidden = ['id']; protected $hidden = ['id'];
protected $casts = [ protected $casts = [
'super' => 'boolean', 'assistant_disable_qr_code' => 'boolean',
'assistant_hide_create_account' => 'boolean',
'assistant_hide_third_party_account' => 'boolean',
'carddav_user_credentials' => 'boolean',
'disable_broadcast_feature' => 'boolean',
'disable_call_recordings_feature' => 'boolean',
'disable_chat_feature' => 'boolean', 'disable_chat_feature' => 'boolean',
'disable_meetings_feature' => 'boolean', 'disable_meetings_feature' => 'boolean',
'disable_broadcast_feature' => 'boolean',
'hide_settings' => 'boolean',
'hide_account_settings' => 'boolean',
'disable_call_recordings_feature' => 'boolean',
'only_display_sip_uri_username' => 'boolean',
'assistant_hide_create_account' => 'boolean',
'assistant_disable_qr_code' => 'boolean',
'assistant_hide_third_party_account' => 'boolean',
'expire_at' => 'date', 'expire_at' => 'date',
'hide_account_settings' => 'boolean',
'hide_settings' => 'boolean',
'only_display_sip_uri_username' => 'boolean',
'super' => 'boolean',
]; ];
public const HOST_REGEX = '[\w\-]+'; public const HOST_REGEX = '[\w\-]+';
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)'; public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)';
protected static function booted()
{
static::addGlobalScope('domain', function (Builder $builder) {
if (!Auth::hasUser()) return;
if (Auth::hasUser() || Auth::user()->superAdmin) {
return;
}
$builder->where('domain', Auth::user()->domain);
});
}
public function accounts() public function accounts()
{ {
return $this->hasMany(Account::class, 'domain', 'domain'); return $this->hasMany(Account::class, 'domain', 'domain');
@ -82,6 +97,11 @@ class Space extends Model
return $this->hasOne(SpaceEmailServer::class); return $this->hasOne(SpaceEmailServer::class);
} }
public function carddavServers()
{
return $this->hasMany(SpaceCardDavServer::class);
}
public function scopeNotFull(Builder $query) public function scopeNotFull(Builder $query)
{ {
return $query->where('max_accounts', 0) return $query->where('max_accounts', 0)

View file

@ -0,0 +1,82 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SpaceCardDavServer extends Model
{
protected $hidden = ['space_id'];
protected $table = 'space_carddav_servers';
protected $fillable = ['uri', 'enabled', 'min_characters', 'results_limist', 'use_exact_match_policy', 'timeout', 'delay', 'fields_for_user_input', 'fields_for_domain'];
protected $casts = [
'enabled' => 'boolean',
'use_exact_match_policy' => 'boolean',
];
public function space()
{
return $this->belongsTo(Space::class);
}
public function accounts()
{
return $this->belongsToMany(Account::class, 'account_carddav_credentials', 'space_carddav_server_id', 'account_id');
}
public function getNameAttribute()
{
return __('CardDav Server') . ' ' . $this->id;
}
public function getProvisioningSection($config, int $remoteContactDirectoryCounter)
{
$dom = $config->ownerDocument;
$section = $dom->createElement('section');
$section->setAttribute('name', 'remote_contact_directory_' . $remoteContactDirectoryCounter);
$entry = $dom->createElement('entry', $this->enabled ? '1': '0');
$entry->setAttribute('name', 'enabled');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'carddav');
$entry->setAttribute('name', 'type');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->uri);
$entry->setAttribute('name', 'uri');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->min_characters);
$entry->setAttribute('name', 'min_characters');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->results_limit);
$entry->setAttribute('name', 'results_limit');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->timeout);
$entry->setAttribute('name', 'timeout');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->delay);
$entry->setAttribute('name', 'delay');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->fields_for_user_input);
$entry->setAttribute('name', 'carddav_fields_for_user_input');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->fields_for_domain);
$entry->setAttribute('name', 'carddav_fields_for_domain');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $this->use_exact_match_policy ? '1': '0');
$entry->setAttribute('name', 'carddav_use_exact_match_policy');
$section->appendChild($entry);
$config->appendChild($section);
}
}

View file

@ -18,7 +18,7 @@ return [
| |
*/ */
'driver' => env('SESSION_DRIVER', 'cookie'), 'driver' => env('SESSION_DRIVER', 'file'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -0,0 +1,62 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('spaces', function (Blueprint $table) {
$table->boolean('carddav_user_credentials')->default(false);
});
Schema::create('space_carddav_servers', function (Blueprint $table) {
$table->id();
$table->bigInteger('space_id')->unsigned();
$table->foreign('space_id')->references('id')
->on('spaces')->onDelete('cascade');
$table->boolean('enabled')->default(true);
$table->string('uri');
$table->integer('min_characters')->default(3);
$table->integer('results_limit')->default(0);
$table->integer('timeout')->default(5);
$table->integer('delay')->default(500);
$table->string('fields_for_user_input')->nullable();
$table->string('fields_for_domain')->nullable();
$table->boolean('use_exact_match_policy')->default(false);
$table->timestamps();
});
Schema::create('account_carddav_credentials', function (Blueprint $table) {
$table->string('username', 64);
$table->string('password', 255);
$table->string('domain', 255);
$table->string('algorithm', 10)->default('MD5');
$table->bigInteger('space_carddav_server_id')->unsigned();
$table->foreign('space_carddav_server_id')->references('id')
->on('space_carddav_servers')->onDelete('cascade');
$table->integer('account_id')->unsigned();
$table->foreign('account_id')->references('id')
->on('accounts')->onDelete('cascade');
$table->timestamps();
$table->unique(['space_carddav_server_id', 'account_id'], 'account_carddav_credentials_unique');
});
}
public function down(): void
{
Schema::table('spaces', function (Blueprint $table) {
$table->dropColumn('carddav_user_credentials');
});
Schema::dropIfExists('account_carddav_credentials');
Schema::dropIfExists('space_carddav_servers');
}
};

View file

@ -41,6 +41,9 @@
"Calls logs": "Journaux d'appel", "Calls logs": "Journaux d'appel",
"Cancel": "Annuler", "Cancel": "Annuler",
"Cannot be changed once created.": "Ne peut être changé par la suite.", "Cannot be changed once created.": "Ne peut être changé par la suite.",
"CardDav credentials": "Identification CardDav",
"CardDav Server": "Serveur CardDav",
"CardDav Servers": "Serveurs CardDav",
"Change your email": "Changer votre email", "Change your email": "Changer votre email",
"Change your phone number": "Changer votre numéro de téléphone", "Change your phone number": "Changer votre numéro de téléphone",
"Check the README.md documentation": "Voir la documentation dans README.md", "Check the README.md documentation": "Voir la documentation dans README.md",
@ -71,6 +74,7 @@
"Day": "Jour", "Day": "Jour",
"Deactivate All": "Tout désactiver", "Deactivate All": "Tout désactiver",
"Deactivated": "Désactivé", "Deactivated": "Désactivé",
"Delay in milliseconds before submiting the request": "Temps en millisecondes avant d'envoyer la requête",
"Delete": "Supprimer", "Delete": "Supprimer",
"Description": "Description", "Description": "Description",
"Devices": "Appareils", "Devices": "Appareils",
@ -84,6 +88,7 @@
"Email": "Email", "Email": "Email",
"Empty": "Vide", "Empty": "Vide",
"Enable the web interface": "Activer l'interface web", "Enable the web interface": "Activer l'interface web",
"Enable user credentials for CardDav servers": "Activer les identifiants utilisateur pour les serveurs CardDav",
"Enabled": "Activé", "Enabled": "Activé",
"Encrypted": "Chiffré", "Encrypted": "Chiffré",
"Enter the code you received below": "Saisissez le code reçu ci-dessous", "Enter the code you received below": "Saisissez le code reçu ci-dessous",
@ -118,6 +123,9 @@
"Key": "Clef", "Key": "Clef",
"Last used": "Dernière utilisation", "Last used": "Dernière utilisation",
"Leave empty to create a root Space.": "Laisser vide si vous souhaitez créer un Espace à la racine", "Leave empty to create a root Space.": "Laisser vide si vous souhaitez créer un Espace à la racine",
"Limit the number of results": "Limiter le nomber de résultats",
"List of vcard fields to match for SIP domain": "Liste des champs vcard à matcher pour le domaine SIP",
"List of vcard fields to match with user input": "Liste des champs vcard à matcher avec les entrées utilisateur",
"Login to my account":"Connexion à mon compte", "Login to my account":"Connexion à mon compte",
"Login using a QRCode": "S'authentifier avec un QRCode", "Login using a QRCode": "S'authentifier avec un QRCode",
"Login": "Authentification", "Login": "Authentification",
@ -125,6 +133,7 @@
"Markdown text": "Texte Markdown", "Markdown text": "Texte Markdown",
"Max accounts": "Maximum de comptes", "Max accounts": "Maximum de comptes",
"Meeting": "Réunions", "Meeting": "Réunions",
"Min characters to search": "Nombre minimum de caractères pour chercher",
"Month": "Mois", "Month": "Mois",
"My Account": "Mon Compte", "My Account": "Mon Compte",
"My Space": "Mon Espace", "My Space": "Mon Espace",
@ -168,6 +177,7 @@
"Remote provisioning": "Configuration distante", "Remote provisioning": "Configuration distante",
"Remove": "Remove", "Remove": "Remove",
"Renew": "Renouveller", "Renew": "Renouveller",
"Request timeout in seconds": "Expiration de la requête en secondes",
"Requests": "Requêtes", "Requests": "Requêtes",
"Resend": "Renvoyer", "Resend": "Renvoyer",
"Reset my password":"Réinitialiser mon mot de passe", "Reset my password":"Réinitialiser mon mot de passe",
@ -185,6 +195,7 @@
"Send an email to the user to reset the password": "Envoyer un email à l'utilisateur pour réinitialiser son mot de passe", "Send an email to the user to reset the password": "Envoyer un email à l'utilisateur pour réinitialiser son mot de passe",
"Send an email to the user with provisioning information": "Envoyer un email à l'utilisateur avec les informations de déploiement", "Send an email to the user with provisioning information": "Envoyer un email à l'utilisateur avec les informations de déploiement",
"Send": "Envoyer", "Send": "Envoyer",
"Separated by commas": "Séparé par des virgules",
"Settings": "Paramètres", "Settings": "Paramètres",
"Show usernames only": "Afficher uniquement les noms d'utilisateur", "Show usernames only": "Afficher uniquement les noms d'utilisateur",
"SIP address":"Adresse SIP", "SIP address":"Adresse SIP",
@ -224,6 +235,7 @@
"Updated on": "Mis à jour le", "Updated on": "Mis à jour le",
"Updated": "Mise à jour", "Updated": "Mise à jour",
"Use ; to comment, key=\"value\" to declare a complex string.": "Utilisez ; pour commenter, clef=\"valeur\" pour déclarer une chaine complexe.", "Use ; to comment, key=\"value\" to declare a complex string.": "Utilisez ; pour commenter, clef=\"valeur\" pour déclarer une chaine complexe.",
"Use exact match policy": "Utiliser la rêgle de recherche exacte",
"Use the mobile app to recover your account using your phone number": "Utilisez l'application mobile pour récupérer votre compte avec votre numéro de téléphone", "Use the mobile app to recover your account using your phone number": "Utilisez l'application mobile pour récupérer votre compte avec votre numéro de téléphone",
"Used on": "Utilisé le", "Used on": "Utilisé le",
"User": "Utilisateur", "User": "Utilisateur",
@ -243,6 +255,7 @@
"Welcome on :app_name": "Bienvenue sur :app_name", "Welcome on :app_name": "Bienvenue sur :app_name",
"Welcome to :space: Start using your account today": "Bienvenue sur :space : commencez à utiliser votre compte dès maintenant", "Welcome to :space: Start using your account today": "Bienvenue sur :space : commencez à utiliser votre compte dès maintenant",
"Welcome to :space":"Bienvenue sur :space", "Welcome to :space":"Bienvenue sur :space",
"Whether match must be exact or approximate (ignoring case, accents…)": "Est-ce que la recherche doit être exacte ou approximative (ignorer les majuscules, les accents…)",
"Year": "Année", "Year": "Année",
"You already have an account?": "Vous avez déjà un compte ?", "You already have an account?": "Vous avez déjà un compte ?",
"You are going to permanently delete the following element. Please confirm your action.": "Vous allez supprimer l'élément suivant. Veuillez confirmer votre action.", "You are going to permanently delete the following element. Please confirm your action.": "Vous allez supprimer l'élément suivant. Veuillez confirmer votre action.",

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page"> <li class="breadcrumb-item active" aria-current="page">
{{ __('Types') }} {{ __('Types') }}
</li> </li>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page"> <li class="breadcrumb-item active" aria-current="page">
{{ __('Actions') }} {{ __('Actions') }}
</li> </li>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active"> <li class="breadcrumb-item active">
{{ __('Actions') }} {{ __('Actions') }}
</li> </li>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">Activity</li> <li class="breadcrumb-item active" aria-current="page">Activity</li>
@endsection @endsection

View file

@ -0,0 +1,54 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">
{{ __('CardDav credentials') }}
</li>
@endsection
@section('content')
<h1><i class="ph ph-plus"></i> {{ __('CardDav credentials') }}</h1>
<form method="POST" action="{{ route('admin.account.carddavs.store', $account->id) }}" accept-charset="UTF-8">
@csrf
<div class="select large">
<select name="carddav_id">
@foreach ($carddavServers as $carddavServer)
<option value="{{ $carddavServer->id }}">{{ $carddavServer->name }}</option>
@endforeach
</select>
<label for="carddav_id">{{ __('CardDav Server') }}</label>
@include('parts.errors', ['name' => 'carddav_id'])
</div>
<div>
<input placeholder="Username" name="username" type="text" value="{{ old('username') }}" required>
<label for="username">{{ __('Username') }}</label>
@include('parts.errors', ['name' => 'username'])
</div>
<div>
<input placeholder="Username" name="domain" type="text" value="{{ old('domain') }}" required>
<label for="domain">{{ __('Domain') }}</label>
@include('parts.errors', ['name' => 'domain'])
</div>
<div>
<input placeholder="Password" name="password" type="password" required>
<label for="password">{{ __('Password') }}</label>
@include('parts.errors', ['name' => 'password'])
</div>
<div class="select">
<select name="algorithm">
@foreach (passwordAlgorithms() as $value => $key)
<option value="{{ $value }}">{{ $value }}</option>
@endforeach
</select>
<label for="algorithm">{{ __('Algorithm') }}</label>
</div>
<div>
<input class="btn btn-success" type="submit" value="{{ __('Add') }}">
</div>
</form>
@endsection

View file

@ -0,0 +1,31 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
<li class="breadcrumb-item active">
{{ __('CardDav credentials') }}
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection
@section('content')
<h1><i class="ph ph-trash"></i> {{ __('CardDav credentials') }}</h1>
<form method="POST" action="{{ route('admin.account.carddavs.destroy', [$account, $carddavCredentials->cardDavServer]) }}"
accept-charset="UTF-8">
@csrf
@method('delete')
<div>
<p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p>
<p><b>{{ $carddavCredentials->identifier }}</b></p>
</div>
<input name="account_id" type="hidden" value="{{ $account->id }}">
<input name="carddav_id" type="hidden" value="{{ $carddavCredentials->cardDavServer->id }}">
<div>
<input class="btn" type="submit" value="{{ __('Delete') }}">
</div>
</form>
@endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Add contact') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Add contact') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active"> <li class="breadcrumb-item active">
{{ __('Contacts') }} {{ __('Contacts') }}
</li> </li>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Contacts') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Contacts') }}</li>
@endsection @endsection

View file

@ -1,9 +1,11 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
@if ($account->id) @if ($account->id)
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account]) <li class="breadcrumb-item">
<a href="{{ route('admin.account.show', $account->id) }}">{{ $account->identifier }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Edit') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Edit') }}</li>
@else @else
<li class="breadcrumb-item active" aria-current="page">{{ __('Create') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Create') }}</li>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection @endsection

View file

@ -1,13 +1,12 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection @endsection
@section('content') @section('content')
<h2>{{ __('Delete') }}</h2> <h1><i class="ph ph-trash"></i> {{ __('Delete') }}</h1>
<div> <div>
<p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p> <p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p>

View file

@ -1,17 +1,12 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Dictionary') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Dictionary') }}</li>
@endsection @endsection
@section('content') @section('content')
@if ($entry->id) <h1><i class="ph @if ($entry->id)ph-pencil @else ph-plus @endif"></i> {{ __('Dictionary') }}</h1>
<h2>{{ __('Edit') }}</h2>
@else
<h2>{{ __('Create') }}</h2>
@endif
<form method="POST" <form method="POST"
action="{{ $entry->id ? route('admin.account.dictionary.update', [$entry->account, $entry]) : route('admin.account.dictionary.store', $account) }}" action="{{ $entry->id ? route('admin.account.dictionary.update', [$entry->account, $entry]) : route('admin.account.dictionary.store', $account) }}"

View file

@ -1,13 +1,12 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Dictionary') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Dictionary') }}</li>
@endsection @endsection
@section('content') @section('content')
<h2>{{ __('Delete') }}</h2> <h1><i class="ph ph-trash"></i> {{ __('Delete') }}</h1>
<div> <div>
<p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p> <p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}</p>

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item"><a href="{{ route('admin.account.external.show', ['account' => $account]) }}">{{ __('External Account') }}</a></li> <li class="breadcrumb-item"><a href="{{ route('admin.account.external.show', ['account' => $account]) }}">{{ __('External Account') }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active">{{ __('External Account') }}</li> <li class="breadcrumb-item active">{{ __('External Account') }}</li>
@endsection @endsection

View file

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item active" aria-current="page">{{ __('Import') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Import') }}</li>
@endsection @endsection

View file

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item active" aria-current="page">{{ __('Import') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Import') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Provisioning') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Provisioning') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Reset password') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Reset password') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
@endsection @endsection
@section('content') @section('content')
@ -81,7 +80,7 @@
<div class="card"> <div class="card">
<a class="btn small oppose" href="{{ route('admin.account.external.show', $account) }}"> <a class="btn small oppose" href="{{ route('admin.account.external.show', $account) }}">
<i class="ph ph-pencil"></i> <i class="ph ph-plus"></i>
@if ($account->external){{ __('Edit') }}@else{{ __('Create') }}@endif @if ($account->external){{ __('Edit') }}@else{{ __('Create') }}@endif
</a> </a>
<h3> <h3>
@ -130,6 +129,53 @@
@endif @endif
</div> </div>
@if ($account->space->carddav_user_credentials)
<div class="card large" id="carddavs">
@if ($account->remainingCardDavCredentialsCreatable->count() > 0)
<a class="btn small oppose" href="{{ route('admin.account.carddavs.create', $account) }}">
<i class="ph ph-plus"></i>
{{ __('Add') }}
</a>
@endif
<h3>
{{ __('CardDav credentials') }}
</h3>
<table>
<thead>
<tr>
<th>{{ __('CardDav Server') }}</th>
<th>{{ __('Username') }}</th>
<th>{{ __('Domain') }}</th>
<th>{{ __('Algorithm') }}</th>
<th></th>
</tr>
</thead>
<tbody>
@if ($account->carddavServers->isEmpty())
<tr class="empty">
<td colspan="5">{{ __('Empty') }}</td>
</tr>
@else
@foreach ($account->carddavServers as $carddavServer)
<tr>
<td class="line">{{ $carddavServer->name }}</td>
<td class="line">{{ $carddavServer->pivot->username }}</td>
<td class="line">{{ $carddavServer->pivot->domain }}</td>
<td class="line">{{ $carddavServer->pivot->algorithm }}</td>
<td class="actions">
<a type="button" class="btn small tertiary" href="{{ route('admin.account.carddavs.delete', [$account, $carddavServer]) }}">
<i class="ph ph-trash"></i>
</a>
</td>
</tr>
@endforeach
@endif
</tbody>
</table>
</div>
@endif
<div class="card large"> <div class="card large">
<h3> <h3>
{{ __('Devices') }} {{ __('Devices') }}
@ -144,7 +190,7 @@
<tbody> <tbody>
@if ($devices->isEmpty()) @if ($devices->isEmpty())
<tr class="empty"> <tr class="empty">
<td colspan="3">{{ __('Empty') }}</td> <td colspan="2">{{ __('Empty') }}</td>
</tr> </tr>
@else @else
@foreach ($devices as $device) @foreach ($devices as $device)

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Statistics') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Statistics') }}</li>
@endsection @endsection

View file

@ -1,8 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
@include('admin.account.parts.breadcrumb_accounts_show', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">{{ __('Calls logs') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Calls logs') }}</li>
@endsection @endsection

View file

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a> <a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a>
</li> </li>

View file

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a> <a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a>
</li> </li>

View file

@ -1,7 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index') @include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item" aria-current="page"> <li class="breadcrumb-item" aria-current="page">
<a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a> <a href="{{ route('admin.account.type.index') }}">{{ __('Types') }}</a>
</li> </li>

View file

@ -1,3 +1,4 @@
@include('admin.parts.breadcrumb.accounts.index')
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{{ route('admin.account.show', $account->id) }}">{{ $account->identifier }}</a> <a href="{{ route('admin.account.show', $account->id) }}">{{ $account->identifier }}</a>
</li> </li>

View file

@ -0,0 +1,5 @@
@if (auth()->user()->superAdmin)
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
@endif

View file

@ -0,0 +1,4 @@
@include('admin.parts.breadcrumb.spaces.show')
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.integration', $space) }}">{{ __('Integration') }}</a>
</li>

View file

@ -0,0 +1,8 @@
@include('admin.parts.breadcrumb.spaces.index')
<li class="breadcrumb-item">
@if (auth()->user()->superAdmin)
<a href="{{ route('admin.spaces.show', $space) }}">{{ $space->name }}</a>
@else
{{ $space->name }}
@endif
</li>

View file

@ -1,14 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.show')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space->id) }}">
{{ $space->name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Administration') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Administration') }}</li>
@endsection @endsection
@ -48,6 +41,12 @@
@include('parts.form.toggle', ['object' => $space, 'key' => 'super', 'label' => __('Super Space'), 'supporting' => __('All the admins will be super admins')]) @include('parts.form.toggle', ['object' => $space, 'key' => 'super', 'label' => __('Super Space'), 'supporting' => __('All the admins will be super admins')])
</div> </div>
<h3 class="large">{{ __('Integration') }}</h3>
<div class="large">
@include('parts.form.toggle', ['object' => $space, 'key' => 'carddav_user_credentials', 'label' => __('Enable user credentials for CardDav servers')])
</div>
<h3 class="large">Interface</h3> <h3 class="large">Interface</h3>
<div> <div>
@include('parts.form.toggle', ['object' => $space, 'key' => 'custom_theme', 'label' => __('Allow a custom CSS theme'), 'supporting' => __('Check the README.md documentation')]) @include('parts.form.toggle', ['object' => $space, 'key' => 'custom_theme', 'label' => __('Allow a custom CSS theme'), 'supporting' => __('Check the README.md documentation')])

View file

@ -0,0 +1,98 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.parts.breadcrumb.spaces.integration')
<li class="breadcrumb-item active" aria-current="page">{{ __('CardDav Server') }}</li>
@endsection
@section('content')
<header>
<h1>
<i class="ph ph-users"></i> {{ __('CardDav Server') }} -
@if ($carddavServer->id)
{{ __('Edit') }}
@else
{{ __('Create') }}
@endif
</h1>
</header>
<form method="POST"
action="{{ $carddavServer->id ? route('admin.spaces.carddavs.update', [$space->id, $carddavServer->id]) : route('admin.spaces.carddavs.store', $space->id) }}"
id="create_edit" accept-charset="UTF-8">
@csrf
@method($carddavServer->id ? 'put' : 'post')
<div>
<input placeholder="https://..." required="required" name="uri" type="url"
value="@if($carddavServer->uri){{ $carddavServer->uri }}@else{{ old('uri') }}@endif">
<label for="uri">Uri</label>
@include('parts.errors', ['name' => 'uri'])
</div>
@include('parts.form.toggle', ['object' => $carddavServer, 'key' => 'enabled', 'label' => __('Enabled')])
<hr class="large" />
<div>
<input placeholder="3" min="1" required="required" name="min_characters" type="number" value="@if($carddavServer->min_characters){{ $carddavServer->min_characters }}@else{{ old('min_characters') ?? 3 }}@endif">
<label for="min_characters">{{ __('Min characters to search') }}</label>
@include('parts.errors', ['name' => 'min_characters'])
</div>
<div>
<input placeholder="0" min="0" required="required" name="results_limit" type="number" value="@if($carddavServer->results_limit){{ $carddavServer->results_limit }}@else{{ old('results_limit') ?? 0 }}@endif">
<label for="results_limit">{{ __('Limit the number of results') }}</label>
<span class="supporting">{{ __('Unlimited if set to 0') }}</span>
@include('parts.errors', ['name' => 'results_limit'])
</div>
@include('parts.form.toggle', ['object' => $carddavServer, 'key' => 'use_exact_match_policy', 'label' => __('Use exact match policy'), 'supporting' => __('Whether match must be exact or approximate (ignoring case, accents…)')])
<details class="large" @if ($errors->isNotEmpty())open @endif>
<summary>
<h3 class="large">
{{ __('Other information') }}
</h3>
</summary>
<section>
<div>
<input placeholder="5" min="1" required="required" name="timeout" type="number" value="@if($carddavServer->timeout){{ $carddavServer->timeout }}@else{{ old('timeout') ?? 3 }}@endif">
<label for="timeout">{{ __('Request timeout in seconds') }}</label>
@include('parts.errors', ['name' => 'timeout'])
</div>
<div>
<input placeholder="500" min="100" required="required" name="delay" type="number" value="@if($carddavServer->delay){{ $carddavServer->delay }}@else{{ old('delay') ?? 500 }}@endif">
<label for="delay">{{ __('Delay in milliseconds before submiting the request') }}</label>
@include('parts.errors', ['name' => 'delay'])
</div>
<div>
<input placeholder="username,domain..." name="fields_for_user_input" type="text"
value="{{ $carddavServer->fields_for_user_input ?? old('fields_for_user_input') }}">
<label for="fields_for_user_input">{{ __('List of vcard fields to match with user input') }}</label>
<span class="supporting">{{ __('Separated by commas') }}</span>
@include('parts.errors', ['name' => 'fields_for_user_input'])
</div>
<div>
<input placeholder="username,domain..." name="fields_for_domain" type="text"
value="{{ $carddavServer->fields_for_domain ?? old('fields_for_domain') }}">
<label for="fields_for_domain">{{ __('List of vcard fields to match for SIP domain') }}</label>
<span class="supporting">{{ __('Separated by commas') }}</span>
@include('parts.errors', ['name' => 'fields_for_domain'])
</div>
</section>
</details>
<div class="large">
@if ($carddavServer->id)
<input class="btn" type="submit" value="{{ __('Update') }}">
@else
<input form="create_edit" class="btn" type="submit" value="{{ __('Create') }}">
@endif
</div>
</form>
@endsection

View file

@ -0,0 +1,28 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.parts.breadcrumb.spaces.integration')
<li class="breadcrumb-item active" aria-current="page">{{ __('CardDav Server') }} - {{ __('Delete' ) }}</li>
@endsection
@section('content')
<header>
<h1><i class="ph ph-trash"></i> {{ __('CardDav Server') }} - {{ __('Delete') }}</h1>
<a href="{{ route('admin.spaces.integration', ['space' => $space]) }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">
</header>
<form id="delete" method="POST" action="{{ route('admin.spaces.carddavs.destroy', [$carddavServer->space_id, $carddavServer->id]) }}" accept-charset="UTF-8">
@csrf
@method('delete')
<div class="large">
<p>{{ __('You are going to permanently delete the following element. Please confirm your action.') }}<br />
<b><i class="ph ph-identification-card"></i> {{ $carddavServer->uri }}</b>
</p>
<input name="account_id" type="hidden" value="{{ $carddavServer->space_id }}">
</div>
<div>
</div>
</form>
@endsection

View file

@ -2,14 +2,7 @@
@section('breadcrumb') @section('breadcrumb')
@if (auth()->user()->superAdmin) @if (auth()->user()->superAdmin)
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.show')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space->id) }}">
{{ $space->name }}
</a>
</li>
@else @else
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{{ route('admin.spaces.me') }}"> <a href="{{ route('admin.spaces.me') }}">

View file

@ -1,9 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.index')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Create') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Create') }}</li>
@endsection @endsection

View file

@ -1,9 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.show')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Delete') }}</li>
@endsection @endsection

View file

@ -1,14 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.show')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space->id) }}">
{{ $space->name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('App Configuration') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('App Configuration') }}</li>
@endsection @endsection

View file

@ -1,23 +1,13 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.integration')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space) }}">
{{ $space->name }}
</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.integration', $space) }}">{{ __('Integration') }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Email Server') }} - {{ __('Delete' ) }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Email Server') }} - {{ __('Delete' ) }}</li>
@endsection @endsection
@section('content') @section('content')
<header> <header>
<h1><i class="ph ph-trash"></i> {{ __('Delete') }}</h1> <h1><i class="ph ph-trash"></i> {{ __('Email Server') }} - {{ __('Delete') }}</h1>
<a href="{{ route('admin.spaces.integration', ['space' => $space]) }}" class="btn secondary oppose">{{ __('Cancel') }}</a> <a href="{{ route('admin.spaces.integration', ['space' => $space]) }}" class="btn secondary oppose">{{ __('Cancel') }}</a>
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}"> <input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">

View file

@ -1,17 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.integration')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space) }}">
{{ $space->name }}
</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.integration', $space) }}">{{ __('Integration') }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Email Server') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Email Server') }}</li>
@endsection @endsection
@ -20,7 +10,6 @@
<h1><i class="ph ph-envelope"></i> {{ $space->name }}</h1> <h1><i class="ph ph-envelope"></i> {{ $space->name }}</h1>
</header> </header>
<form method="POST" <form method="POST"
action="{{ route('admin.spaces.email.store', $space->id) }}" action="{{ route('admin.spaces.email.store', $space->id) }}"
id="show" accept-charset="UTF-8"> id="show" accept-charset="UTF-8">

View file

@ -1,15 +1,7 @@
@extends('layouts.main') @extends('layouts.main')
@section('breadcrumb') @section('breadcrumb')
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.integration')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('admin.spaces.show', $space->id) }}">
{{ $space->name }}
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Integration') }}</li>
@endsection @endsection
@section('content') @section('content')
@ -36,4 +28,29 @@
</p> </p>
</div> </div>
</div> </div>
<br />
<a class="btn small oppose" href="{{ route('admin.spaces.carddavs.create', $space) }}">
<i class="ph ph-plus"></i>
{{ __('Create') }}
</a>
<h3>{{ __('CardDav Servers') }}</h3>
<div class="grid third">
@foreach ($space->carddavServers as $carddavServer)
<div class="card">
<small class="oppose"><i class="ph ph-users"></i> {{ $carddavServer->accounts()->count() }}</small>
<span class="icon"><i class="ph ph-identification-card"></i></span>
<h3>{{ $carddavServer->name }}</h3>
<p>
{{ $carddavServer->uri}}<br />
<br />
<a class="btn oppose" href="{{ route('admin.spaces.carddavs.edit', [$space, $carddavServer]) }}">{{ __('Edit') }}</a>
<a class="btn oppose tertiary" href="{{ route('admin.spaces.carddavs.delete', [$space, $carddavServer]) }}">{{ __('Delete') }}</a>
</p>
</div>
@endforeach
</div>
@endsection @endsection

View file

@ -2,9 +2,7 @@
@section('breadcrumb') @section('breadcrumb')
@if (auth()->user()->superAdmin) @if (auth()->user()->superAdmin)
<li class="breadcrumb-item"> @include('admin.parts.breadcrumb.spaces.index')
<a href="{{ route('admin.spaces.index') }}">{{ __('Spaces') }}</a>
</li>
@endif @endif
<li class="breadcrumb-item">{{ $space->name }}</li> <li class="breadcrumb-item">{{ $space->name }}</li>
<li class="breadcrumb-item active" aria-current="page">{{ __('Information') }}</li> <li class="breadcrumb-item active" aria-current="page">{{ __('Information') }}</li>

View file

@ -0,0 +1,115 @@
# About & Auth.
An API to deal with the Flexisip server.
The API is available under `/api`
A `content-type` and `accept` HTTP headers are REQUIRED to use the API properly
```
> GET /api/{endpoint}
> content-type: application/json
> accept: application/json
```
<div class="card">
Restricted endpoints are protected using a DIGEST authentication or an API Key mechanisms.
### Access model
The endpoints are accessible using three different models:
- <span class="badge badge-success">Public</span> publicly accessible
- <span class="badge badge-info">User</span> the endpoint can only be accessed by an authenticated user
- <span class="badge badge-warning">Admin</span> the endpoint can be only be accessed by an authenticated admin user
- <span class="badge badge-error">Super Admin</span> the endpoint can be only be accessed by an authenticated super admin user
### Space expiration
<span class="badge badge-error">Super Admin</span> can configure and expiration date on Spaces (`expire_at`). If the Space is expired all the authenticated endpoint of the API will return `403`.
### Localization
You can add an [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header to your request to translate the responses, and especially errors messages, in a specific language.
Currently supported languages: @php
echo implode(', ', config('app.authorized_locales'))
@endphp
```
> GET /api/{endpoint}
> Accept-Language: fr
>
< HTTP 422
< {
< "message": "Le champ pseudo est obligatoire.",
< "errors": {
< "username": [
< 0 => "Le champ pseudo est obligatoire."
< ]
< }
< }
```
### Using the API Key
You can retrieve an API Key from your account panel or using <a href="#get-accountsmeapikey">the dedicated API endpoint</a>.
**When generated by a User account the generated API Key will be restricted to the IP that generates it and will be destroyed if not used after some times.**
**If you want a stable API Key, to integrate with another server component for example, you can generate one in the Administration Panel.**
You can then use your freshly generated key by adding a new `x-api-key` header to your API requests:
```
> GET /api/{endpoint}
> x-api-key: {your-api-key}
>
```
Or using a cookie:
```
> GET /api/{endpoint}
> Cookie: x-api-key={your-api-key}
>
```
### Using a JWT token
You can use a <a href="https://jwt.io/">JWT</a> token to authenticate on the API.
To do so you MUST inject it as an `Authorization: Bearer` header and configure the API with the public key of the token emitter.
```
> GET /api/{endpoint}
> Authorization: Bearer {your-jwt-token}
>
```
The API will then check if the token was signed properly, is still valid and authenticate a user that is actually available in the system.
### Using DIGEST
To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.<br />
Only DIGEST-MD5 and DIGEST-SHA-256 are supported through the authentication layer.
A `from` (consisting of the user SIP address, prefixed with `sip:`) header is required to initiate the DIGEST flow.
```
> GET /api/{restricted-endpoint}
> from: sip:foobar@sip.example.org
>
< HTTP 401
< content-type: application/json
< www-authenticate: Digest realm=test,qop=auth,algorithm=MD5,nonce="{nonce}",opaque="{opaque}"
< www-authenticate: Digest realm=test,qop=auth,algorithm=SHA-256,nonce="{nonce}",opaque="{opaque}"
```
You can find more documentation on the related [IETF RFC-7616](https://tools.ietf.org/html/rfc7616).
</div>

View file

@ -0,0 +1,91 @@
## Account Creation Request Tokens
An `account_creation_request_token` is a unique token that can be validated and then used to generate a valid `account_creation_token`.
### `POST /account_creation_request_tokens`
<span class="badge badge-success">Public</span>
Create and return an `account_creation_request_token` that should then be validated to be used.
## Account Creation Tokens
An `account_creation_token` is a unique token that allow the creation or the validation of a unique account.
### `POST /account_creation_tokens/send-by-push`
<span class="badge badge-success">Public</span>
Create and send an `account_creation_token` using a push notification to the device.
Return `403` if a token was already sent, or if the tokens limit is reached for this device.
Return `503` if the token was not successfully sent.
JSON parameters:
* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm
* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters
* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes, underscore and colon characters
### `POST /account_creation_tokens/using-account-creation-request-token`
<span class="badge badge-success">Public</span>
Create an `account_creation_token` using an `account_creation_request_token`.
Return an `account_creation_token`.
Return `404` if the `account_creation_request_token` provided is not valid or expired otherwise.
JSON parameters:
* `account_creation_request_token` **required**
### `POST /account_creation_tokens/consume`
<span class="badge badge-info">User</span>
Consume an `account_creation_token` and link it to the authenticated account.
Return an `account_creation_token`.
Return `404` if the `account_creation_token` provided is not valid.
JSON parameters:
* `account_creation_token` **required**
### `POST /account_creation_tokens`
<span class="badge badge-warning">Admin</span>
Create and return an `account_creation_token`.
## Account Recovery Tokens
An `account_recovery_token` is a unique token that allow the recovery of an account.
It can be used on the following page that also accepts a `phone` optional parameter to prefil the recovery form:
{{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_']) }}
{{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_', 'phone' => '+3312341234']) }}
### `POST /account_recovery_tokens/send-by-push`
<span class="badge badge-success">Public</span>
Create and send an `account_recovery_token` using a push notification to the device.
Return `403` if a token was already sent, or if the tokens limit is reached for this device.
Return `503` if the token was not successfully sent.
JSON parameters:
* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm
* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters
* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes, underscore and colon characters
## Auth Tokens
### `POST /accounts/auth_token`
<span class="badge badge-success">Public</span>
Generate an `auth_token`. To attach the generated token to an account see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach).
Return the `auth_token` object.
### `GET /accounts/auth_token/{auth_token}/attach`
<span class="badge badge-info">User</span>
Attach a publicly generated authentication token to the currently authenticated account.
Return `404` if the token is non existing or invalid.

View file

@ -0,0 +1,176 @@
## Accounts
### `POST /accounts/with-account-creation-token`
<span class="badge badge-success">Public</span>
Create an account using an `account_creation_token`.
Return `422` if the parameters are invalid or if the token is expired.
Return `403` if the `max_accounts` limit of the corresponding Space is reached.
JSON parameters:
* `username` unique username, minimum 6 characters
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `account_creation_token` the unique `account_creation_token`
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
### `GET /accounts/{sip}/info`
<span class="badge badge-success">Public</span>
Retrieve public information about the account.
Return `404` if the account doesn't exists.
### `GET /accounts/me/api_key/{auth_token}`
<span class="badge badge-success">Public</span>
Generate and retrieve a fresh API Key from an `auth_token`. The `auth_token` must be attached to an existing account, see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach) to do so.
Return `404` if the token is invalid or not attached.
This endpoint is also setting the API Key as a Cookie.
### `GET /accounts/me/api_key`
<span class="badge badge-info">User</span>
Generate and retrieve a fresh API Key.
This endpoint is also setting the API Key as a Cookie.
### `GET /accounts/me`
<span class="badge badge-info">User</span>
Retrieve the account information.
### `GET /accounts/me/services/turn`
<span class="badge badge-info">User</span>
If configured, returns valid TURN credentials following the [draft-uberti-behave-turn-rest-00 IEFT Draft](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00).
### `GET /accounts/me/provision`
<span class="badge badge-info">User</span>
Provision the account by generating a fresh `provisioning_token`.
Return the account object.
### `DELETE /accounts/me`
<span class="badge badge-info">User</span>
Delete the account.
### `POST /accounts/me/password`
<span class="badge badge-info">User</span>
Change the account password.
JSON parameters:
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `old_password` **required** if the password is already set, the old password
* `password` **required**, the new password
### `POST /accounts`
<span class="badge badge-warning">Admin</span>
To create an account directly from the API.
Return `403` if the `max_accounts` limit of the corresponding Space is reached.
JSON parameters:
* `username` unique username, minimum 6 characters
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `domain` **not configurable by default**, must exist in one of the configured Spaces. Only configurable if the admin is a super admin. Otherwise the SIP domain of the corresponding space is used.
* `activated` optional, a boolean, set to `false` by default
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default, create an admin account
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
### `PUT /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Update an existing account. Ensure to resend all the parameters to not reset them.
JSON parameters:
* `username` unique username, minimum 6 characters
* `domain` **not configurable by default**, must exist in one of the configured Spaces. Only configurable if the admin is a super admin. Otherwise the SIP domain of the corresponding space is used.
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
Using this endpoint you can also set a fresh dictionnary if the parameter is set. The existing dictionary entries will be destroyed.
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
This endpoint also return the current `phone_change_code` and `email_change_code` if they are available.
### `GET /accounts`
<span class="badge badge-warning">Admin</span>
Retrieve all the accounts, paginated.
### `GET /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Retrieve a specific account.
### `GET /accounts/{sip}/search`
<span class="badge badge-warning">Admin</span>
Search for a specific account by sip address.
### `GET /accounts/{email}/search-by-email`
<span class="badge badge-warning">Admin</span>
Search for a specific account by email.
### `DELETE /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Delete a specific account and its related information.
### `POST /accounts/{id}/activate`
<span class="badge badge-warning">Admin</span>
Activate an account.
### `POST /accounts/{id}/deactivate`
<span class="badge badge-warning">Admin</span>
Deactivate an account.
### `POST /accounts/{id}/block`
<span class="badge badge-warning">Admin</span>
Block an account.
### `POST /accounts/{id}/unblock`
<span class="badge badge-warning">Admin</span>
Unblock an account.
### `GET /accounts/{id}/provision`
<span class="badge badge-warning">Admin</span>
Provision an account by generating a fresh `provisioning_token`.
### `POST /accounts/{id}/send_provisioning_email`
<span class="badge badge-warning">Admin</span>
Send a provisioning email to the account.
### `POST /accounts/{id}/send_reset_password_email`
<span class="badge badge-warning">Admin</span>
Send a password reset email to the account.

View file

@ -0,0 +1,38 @@
## Account actions
The following endpoints will return `403 Forbidden` if the requested account doesn't have a DTMF protocol configured.
### `GET /accounts/{id}/actions`
<span class="badge badge-warning">Admin</span>
Show an account related actions.
### `GET /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Show an account related action.
### `POST /accounts/{id}/actions/`
<span class="badge badge-warning">Admin</span>
Create an account action.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
* `code` **required**, alpha numeric, lowercase
### `PUT /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Create an account action.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
* `code` **required**, alpha numeric, lowercase
### `DELETE /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Delete an account related action.

View file

@ -0,0 +1,30 @@
## Account CardDav credentials
The following endpoints will return `403 Forbidden` if the requested account doesn't have a DTMF protocol configured.
### `GET /accounts/{id}/carddavs`
<span class="badge badge-warning">Admin</span>
Show an account CardDav servers credentials.
### `GET /accounts/{id}/carddavs/{carddav_id}`
<span class="badge badge-warning">Admin</span>
Show an account CardDav server credentials.
### `PUT /accounts/{id}/carddavs/{carddav_id}`
<span class="badge badge-warning">Admin</span>
Create an account CardDav server credentials.
JSON parameters:
* `username` **required** the username
* `password` **required** the password in plain text
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `domain` **required** the domain
### `DELETE /accounts/{id}/carddavs/{carddav_id}`
<span class="badge badge-warning">Admin</span>
Delete an account related action.

View file

@ -0,0 +1,22 @@
## Account contacts
### `GET /accounts/{id/me}/contacts`
<span class="badge badge-info">User</span>
<span class="badge badge-warning">Admin</span>
Return the user contacts.
### `GET /accounts/me/contacts/{sip}`
<span class="badge badge-info">User</span>
Return a user contact.
### `POST /accounts/{id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Add a contact to the list.
### `DELETE /accounts/{id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Remove a contact from the list.

View file

@ -0,0 +1,56 @@
## Contacts Lists
### `GET /contacts_lists`
<span class="badge badge-warning">Admin</span>
Show all the contacts lists.
### `GET /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Show a contacts list.
### `POST /contacts_lists`
<span class="badge badge-warning">Admin</span>
Create a contacts list.
JSON parameters:
* `title` **required**
* `description` **required**
### `PUT /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Update a contacts list.
JSON parameters:
* `title` **required**
* `description` **required**
### `DELETE /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Delete a contacts list.
### `POST /contacts_lists/{contacts_list_id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Add a contact to the contacts list.
### `DELETE /contacts_lists/{contacts_list_id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Remove a contact from the contacts list.
### `POST /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Add a contacts list to the account.
### `DELETE /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Remove a contacts list from the account.

View file

@ -0,0 +1,15 @@
## Account dictionary
### `GET /accounts/{id}/dictionary`
<span class="badge badge-warning">Admin</span>
Get all the account dictionary entries.
### `POST /accounts/{id}/dictionary/{key}`
<span class="badge badge-warning">Admin</span>
Add or update a new entry to the dictionary
JSON parameters:
* `value` **required**, the entry value

View file

@ -0,0 +1,24 @@
## Accounts email
### `POST /accounts/me/email/request`
<span class="badge badge-info">User</span>
Request to change the account email. An email will be sent to the new email address to confirm the operation.
Will return `403` if the account doesn't have a validated <a href='#account-creation-tokens'>Account Creation Token</a> attached to it.
JSON parameters:
* `email` the new email address, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
### `POST /accounts/me/email`
<span class="badge badge-info">User</span>
Confirm the code received and change the email.
Activate the account.
JSON parameters:
* `code` the code received by email
Return the updated account.

View file

@ -0,0 +1,26 @@
## Account external Account
### `GET /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Get the external account.
### `POST /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Create or update the external account.
JSON parameters:
* `username` **required**
* `domain` **required**
* `password` **required**
* `realm` must be different than `domain`
* `registrar` must be different than `domain`
* `outbound_proxy` must be different than `domain`
* `protocol` **required**, must be `UDP`, `TCP` or `TLS`
### `DELETE /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Delete the external account.

View file

@ -0,0 +1,38 @@
## Account phone number
### `POST /accounts/me/phone/request`
<span class="badge badge-info">User</span>
Request a specific code by SMS to change the phone number.
Will return `403` if the account doesn't have a validated <a href='#account-creation-tokens'>Account Creation Token</a> attached to it.
JSON parameters:
* `phone` the phone number to send the SMS
### `POST /accounts/me/phone`
<span class="badge badge-info">User</span>
Confirm the code received and change the phone number.
Activate the account.
JSON parameters:
* `code` the received SMS code
Return the updated account.
## Accounts devices
### `GET /accounts/{id/me}/devices`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return the user registered devices.
### `DELETE /accounts/{id/me}/devices/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Remove one of the user registered devices.

View file

@ -0,0 +1,44 @@
## Account Types
### `GET /account_types`
<span class="badge badge-warning">Admin</span>
Show all the account types.
### `GET /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Show an account type.
### `POST /account_types`
<span class="badge badge-warning">Admin</span>
Create an account type.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
### `PUT /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Update an account type.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
### `DELETE /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Delete an account type.
### `POST /accounts/{id}/types/{type_id}`
<span class="badge badge-warning">Admin</span>
Add a type to the account.
### `DELETE /accounts/{id}/contacts/{type_id}`
<span class="badge badge-warning">Admin</span>
Remove a type from the account.

View file

@ -0,0 +1,39 @@
## Account vCards storage
### `POST /accounts/{id/me}/vcards-storage`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Store a vCard.
JSON parameters:
* `vcard`, mandatory, a valid vCard having a mandatory `UID` parameter that is uniquelly identifying it. This `UID` parameter will then be used to manipulate the vcard through the following endpoints as `uuid`.
### `PUT /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Update a vCard.
JSON parameters:
* `vcard`, mandatory, a valid vCard having a mandatory `UID` parameter that is uniquelly identifying it and is the same as the `uuid` parameter.
### `GET /accounts/{id/me}/vcards-storage`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return the list of stored vCards
### `GET /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return a stored vCard
### `DELETE /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Delete a stored vCard

View file

@ -0,0 +1,95 @@
## Spaces
Manage the list of allowed `spaces`. The admin accounts declared with a `domain` that is a `super` `sip_domain` will become <span class="badge badge-error">Super Admin</span>.
### `GET /spaces`
<span class="badge badge-error">Super Admin</span>
Get the list of declared Spaces.
### `GET /spaces/{domain}`
<span class="badge badge-error">Super Admin</span>
Get a Space.
### `POST /spaces`
<span class="badge badge-error">Super Admin</span>
Create a new `space`.
JSON parameters:
* `name` **required**, the space name
* `domain` **required**, the SIP domain to use, must be unique
* `host` **required**, the space host
* `account_proxy_registrar_address`, the account proxy registrar address
* `account_realm`, the default realm for the accounts, fallback to the domain if not set
* `assistant_disable_qr_code` boolean, disable the QR code feature in the assistant, default to `false`
* `assistant_hide_create_account` boolean, disable the account creation assistant, default to `false`
* `assistant_hide_third_party_account` boolean, disable the call recording feature, default to `false`
* `carddav_user_credentials` boolean, enable credentials for CardDav servers
* `copyright_text` text, the copyright text
* `custom_provisioning_entries` text, the custom configuration used for the provisioning
* `custom_provisioning_overwrite_all` boolean, allow the custom configuration to overwrite the default one
* `custom_theme` boolean, allow a custom CSS file to be loaded
* `disable_broadcast_feature` boolean, disable the broadcast feature, default to `true`
* `disable_call_recordings_feature` boolean, disable the call recording feature, default to `false`
* `disable_chat_feature` boolean, disable the chat feature, default to `false`
* `disable_meetings_feature` boolean, disable the meeting feature, default to `false`
* `expire_at` date, the moment the space is expiring, default to `null` (never expire)
* `hide_account_settings` boolean, disable the account settings, default to `false`
* `hide_settings` boolean, hide the app settings, default to `false`
* `intercom_features` boolean, the intercom features switch
* `intro_registration_text` Markdown text, the main registration page text
* `max_account` integer, the maximum number of accounts configurable in the app, default to `0` (infinite)
* `max_accounts` integer, the maximum number of accounts that can be created in the space, default to `0` (infinite), cannot be less than the actual amount of accounts
* `newsletter_registration_address`, the newsletter registration email address
* `only_display_sip_uri_username` boolean, hide the SIP uris in the app, default to `false`
* `phone_registration` boolean, the phone registration switch
* `provisioning_use_linphone_provisioning_header` boolean
* `public_registration` boolean, the public registration switch
* `super` boolean, set the domain as a Super Domain
* `web_panel` boolean, the web panel switch
### `PUT /spaces/{domain}`
<span class="badge badge-error">Super Admin</span>
Update an existing `sip_domain`.
JSON parameters:
* `account_proxy_registrar_address`, **required**, the account proxy registrar address
* `account_realm`, **required**, the default realm for the accounts, fallback to the domain if not set
* `assistant_disable_qr_code` **required**, boolean
* `assistant_hide_create_account` **required**, boolean
* `assistant_hide_third_party_account` **required**, boolean
* `carddav_user_credentials` **required** boolean, enable credentials for CardDav servers
* `copyright_text` **required**, text, the copyright text
* `custom_provisioning_entries` **required**, text, the custom configuration used for the provisioning
* `custom_provisioning_overwrite_all` **required**, boolean, allow the custom configuration to overwrite the default one
* `custom_theme` **required**, boolean, allow a custom CSS file to be loaded
* `disable_broadcast_feature` **required**, boolean
* `disable_call_recordings_feature` **required**, boolean
* `disable_chat_feature` **required**, boolean
* `disable_meetings_feature` **required**, boolean
* `expire_at` **required**, date, the moment the space is expiring, set to `null` to never expire
* `hide_account_settings` **required**, boolean
* `hide_settings` **required**, boolean
* `intercom_features` **required**, boolean, the intercom features switch
* `intro_registration_text` **required**, Markdown text, the main registration page text
* `max_account` **required**, integer
* `max_accounts` **required**,integer, the maximum number of accounts that can be created in the space, default to `0` (infinite), cannot be less than the actual amount of accounts
* `name` **required**, the space name
* `newsletter_registration_address`, **required**, the newsletter registration email address
* `only_display_sip_uri_username` **required**, boolean
* `phone_registration` **required**, boolean, the phone registration switch
* `provisioning_use_linphone_provisioning_header` **required**, boolean
* `public_registration` **required**, boolean, the public registration switch
* `super` **required**, boolean, set the domain as a Super Domain
* `web_panel` **required**, boolean, the web panel switch
### `DELETE /spaces/{domain}`
<span class="badge badge-error">Super Admin</span>
Delete a domain, **be careful, all the related accounts will also be destroyed**.

View file

@ -0,0 +1,52 @@
## Spaces CardDav Servers
### `GET /spaces/{domain}/carddavs`
<span class="badge badge-error">Super Admin</span>
List current CardDavs servers.
### `GET /spaces/{domain}/carddavs/{id}`
<span class="badge badge-error">Super Admin</span>
Get a specific CardDav server.
### `POST /spaces/{domain}/carddavs`
<span class="badge badge-error">Super Admin</span>
Create a CardDav server configuration.
JSON parameters:
* `uri` **required**, HTTP address of the server
* `port` **required**, integer, the port
* `enabled`, boolean
* `use_exact_match_policy`, boolean, whether match must be exact or approximate
* `min_characters`, integer, min characters to search
* `results_limit`, integer, limit the number of results, 0 to infinite
* `timeout`, integer, request timeout in seconds
* `delay`, integer, delay in milliseconds before submiting the request
* `fields_for_user_input`, comma separated list of vcard fields to match with user input
* `fields_for_domain`, comma separated list of vcard fields to match for SIP domain
### `PUT /spaces/{domain}/carddavs/{id}`
<span class="badge badge-error">Super Admin</span>
Update a CardDav server configuration.
JSON parameters:
* `uri` **required**, HTTP address of the server
* `port` **required**, integer, the port
* `enabled`, boolean
* `use_exact_match_policy`, boolean, whether match must be exact or approximate
* `min_characters`, integer, min characters to search
* `results_limit`, integer, limit the number of results, 0 to infinite
* `timeout`, integer, request timeout in seconds
* `delay`, integer, delay in milliseconds before submiting the request
* `fields_for_user_input`, comma separated list of vcard fields to match with user input
* `fields_for_domain`, comma separated list of vcard fields to match for SIP domain
### `DELETE /spaces/{domain}/carddavs/{id}`
<span class="badge badge-error">Super Admin</span>
Delete a specific CardDav server configuration.

View file

@ -0,0 +1,26 @@
## Spaces Email Server
### `GET /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Get a space email server configuration
### `POST /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Update an existing a space email server configuration.
JSON parameters:
* `host` **required**, the email server hostname
* `port` **required**, integer, the port
* `username`, the username
* `password`, the password
* `from_address`, email address, the sender email address
* `from_name`, the sender name
* `signature`, a text that will end every emails sent
### `DELETE /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Delete the a space email server configuration.

View file

@ -0,0 +1,57 @@
## Statistics
FlexiAPI can record logs generated by the FlexiSIP server and compile them into statistics.
### `POST /statistics/messages`
<span class="badge badge-warning">Admin</span>
Announce the creation of a message.
JSON parameters:
* `id` **required**, string
* `from` **required**, string the sender of the message
* `sent_at` **required**, string, format ISO8601, when the message was actually sent
* `encrypted` **required**, boolean
* `conference_id` string
### `PATCH /statistics/messages/{message_id}/to/{to}/devices/{device_id}`
<span class="badge badge-warning">Admin</span>
Complete a message status.
JSON parameters:
* `last_status` **required**, an integer containing the last status code
* `received_at` **required**, format ISO8601, when the message was received
### `POST /statistics/calls`
<span class="badge badge-warning">Admin</span>
Announce the beginning of a call.
JSON parameters:
* `id` **required**, string
* `from` **required**, string the initier of the call
* `to` **required**, string the destination of the call
* `initiated_at` **required**, string, format ISO8601, when the call was started
* `ended_at` string, format ISO8601, when the call finished
* `conference_id` string
### `PATCH /statistics/calls/{call_id}/devices/{device_id}`
<span class="badge badge-warning">Admin</span>
Complete a call status.
JSON parameters:
* `rang_at` format ISO8601, when the device rang
* `invite_terminated`
* `at` format ISO8601, when the invitation ended
* `state` the termination state
### `PATCH /statistics/calls/{call_id}`
<span class="badge badge-warning">Admin</span>
Update a call when ending.

View file

@ -1,118 +1,4 @@
# About & Auth. @include('api.documentation.about_auth')
An API to deal with the Flexisip server.
The API is available under `/api`
A `content-type` and `accept` HTTP headers are REQUIRED to use the API properly
```
> GET /api/{endpoint}
> content-type: application/json
> accept: application/json
```
<div class="card">
Restricted endpoints are protected using a DIGEST authentication or an API Key mechanisms.
### Access model
The endpoints are accessible using three different models:
- <span class="badge badge-success">Public</span> publicly accessible
- <span class="badge badge-info">User</span> the endpoint can only be accessed by an authenticated user
- <span class="badge badge-warning">Admin</span> the endpoint can be only be accessed by an authenticated admin user
- <span class="badge badge-error">Super Admin</span> the endpoint can be only be accessed by an authenticated super admin user
### Space expiration
<span class="badge badge-error">Super Admin</span> can configure and expiration date on Spaces (`expire_at`). If the Space is expired all the authenticated endpoint of the API will return `403`.
### Localization
You can add an [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header to your request to translate the responses, and especially errors messages, in a specific language.
Currently supported languages: @php
echo implode(', ', config('app.authorized_locales'))
@endphp
```
> GET /api/{endpoint}
> Accept-Language: fr
>
< HTTP 422
< {
< "message": "Le champ pseudo est obligatoire.",
< "errors": {
< "username": [
< 0 => "Le champ pseudo est obligatoire."
< ]
< }
< }
```
### Using the API Key
You can retrieve an API Key from your account panel or using <a href="#get-accountsmeapikey">the dedicated API endpoint</a>.
**When generated by a User account the generated API Key will be restricted to the IP that generates it and will be destroyed if not used after some times.**
**If you want a stable API Key, to integrate with another server component for example, you can generate one in the Administration Panel.**
You can then use your freshly generated key by adding a new `x-api-key` header to your API requests:
```
> GET /api/{endpoint}
> x-api-key: {your-api-key}
>
```
Or using a cookie:
```
> GET /api/{endpoint}
> Cookie: x-api-key={your-api-key}
>
```
### Using a JWT token
You can use a <a href="https://jwt.io/">JWT</a> token to authenticate on the API.
To do so you MUST inject it as an `Authorization: Bearer` header and configure the API with the public key of the token emitter.
```
> GET /api/{endpoint}
> Authorization: Bearer {your-jwt-token}
>
```
The API will then check if the token was signed properly, is still valid and authenticate a user that is actually available in the system.
### Using DIGEST
To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.<br />
Only DIGEST-MD5 and DIGEST-SHA-256 are supported through the authentication layer.
A `from` (consisting of the user SIP address, prefixed with `sip:`) header is required to initiate the DIGEST flow.
```
> GET /api/{restricted-endpoint}
> from: sip:foobar@sip.example.org
>
< HTTP 401
< content-type: application/json
< www-authenticate: Digest realm=test,qop=auth,algorithm=MD5,nonce="{nonce}",opaque="{opaque}"
< www-authenticate: Digest realm=test,qop=auth,algorithm=SHA-256,nonce="{nonce}",opaque="{opaque}"
```
You can find more documentation on the related [IETF RFC-7616](https://tools.ietf.org/html/rfc7616).
</div>
# Endpoints # Endpoints
@ -123,709 +9,35 @@ You can find more documentation on the related [IETF RFC-7616](https://tools.iet
Returns `pong` Returns `pong`
## Spaces @include('api.documentation.spaces')
Manage the list of allowed `spaces`. The admin accounts declared with a `domain` that is a `super` `sip_domain` will become <span class="badge badge-error">Super Admin</span>. @include('api.documentation.spaces.carddav')
### `GET /spaces` @include('api.documentation.spaces.email')
<span class="badge badge-error">Super Admin</span>
Get the list of declared Spaces. @include('api.documentation.account_tokens')
### `GET /spaces/{domain}` @include('api.documentation.accounts')
<span class="badge badge-error">Super Admin</span>
Get a Space. @include('api.documentation.accounts.actions')
### `POST /spaces` @include('api.documentation.accounts.carddav_credentials')
<span class="badge badge-error">Super Admin</span>
Create a new `space`. @include('api.documentation.accounts.contacts_lists')
JSON parameters: @include('api.documentation.accounts.contacts')
* `name` **required**, the space name @include('api.documentation.accounts.dictionary')
* `domain` **required**, the SIP domain to use, must be unique
* `host` **required**, the space host
* `super` boolean, set the domain as a Super Domain
* `disable_chat_feature` boolean, disable the chat feature, default to `false`
* `disable_meetings_feature` boolean, disable the meeting feature, default to `false`
* `disable_broadcast_feature` boolean, disable the broadcast feature, default to `true`
* `hide_settings` boolean, hide the app settings, default to `false`
* `hide_account_settings` boolean, disable the account settings, default to `false`
* `disable_call_recordings_feature` boolean, disable the call recording feature, default to `false`
* `only_display_sip_uri_username` boolean, hide the SIP uris in the app, default to `false`
* `assistant_hide_create_account` boolean, disable the account creation assistant, default to `false`
* `assistant_disable_qr_code` boolean, disable the QR code feature in the assistant, default to `false`
* `assistant_hide_third_party_account` boolean, disable the call recording feature, default to `false`
* `max_account` integer, the maximum number of accounts configurable in the app, default to `0` (infinite)
* `max_accounts` integer, the maximum number of accounts that can be created in the space, default to `0` (infinite), cannot be less than the actual amount of accounts
* `expire_at` date, the moment the space is expiring, default to `null` (never expire)
* `copyright_text` text, the copyright text
* `intro_registration_text` Markdown text, the main registration page text
* `newsletter_registration_address`, the newsletter registration email address
* `account_proxy_registrar_address`, the account proxy registrar address
* `account_realm`, the default realm for the accounts, fallback to the domain if not set
* `custom_provisioning_entries` text, the custom configuration used for the provisioning
* `custom_provisioning_overwrite_all` boolean, allow the custom configuration to overwrite the default one
* `provisioning_use_linphone_provisioning_header` boolean
* `custom_theme` boolean, allow a custom CSS file to be loaded
* `web_panel` boolean, the web panel switch
* `public_registration` boolean, the public registration switch
* `phone_registration` boolean, the phone registration switch
* `intercom_features` boolean, the intercom features switch
### `PUT /spaces/{domain}` @include('api.documentation.accounts.email')
<span class="badge badge-error">Super Admin</span>
Update an existing `sip_domain`. @include('api.documentation.accounts.external_account')
JSON parameters: @include('api.documentation.accounts.phone')
* `name` **required**, the space name @include('api.documentation.accounts.types')
* `super` **required**, boolean, set the domain as a Super Domain
* `disable_chat_feature` **required**, boolean
* `disable_meetings_feature` **required**, boolean
* `disable_broadcast_feature` **required**, boolean
* `hide_settings` **required**, boolean
* `hide_account_settings` **required**, boolean
* `disable_call_recordings_feature` **required**, boolean
* `only_display_sip_uri_username` **required**, boolean
* `assistant_hide_create_account` **required**, boolean
* `assistant_disable_qr_code` **required**, boolean
* `assistant_hide_third_party_account` **required**, boolean
* `max_account` **required**, integer
* `max_accounts` **required**,integer, the maximum number of accounts that can be created in the space, default to `0` (infinite), cannot be less than the actual amount of accounts
* `expire_at` **required**, date, the moment the space is expiring, set to `null` to never expire
* `copyright_text` **required**, text, the copyright text
* `intro_registration_text` **required**, Markdown text, the main registration page text
* `newsletter_registration_address`, **required**, the newsletter registration email address
* `account_proxy_registrar_address`, **required**, the account proxy registrar address
* `account_realm`, **required**, the default realm for the accounts, fallback to the domain if not set
* `custom_provisioning_entries` **required**, text, the custom configuration used for the provisioning
* `custom_provisioning_overwrite_all` **required**, boolean, allow the custom configuration to overwrite the default one
* `provisioning_use_linphone_provisioning_header` **required**, boolean
* `custom_theme` **required**, boolean, allow a custom CSS file to be loaded
* `web_panel` **required**, boolean, the web panel switch
* `public_registration` **required**, boolean, the public registration switch
* `phone_registration` **required**, boolean, the phone registration switch
* `intercom_features` **required**, boolean, the intercom features switch
### `DELETE /spaces/{domain}` @include('api.documentation.accounts.vcards_storage')
<span class="badge badge-error">Super Admin</span>
Delete a domain, **be careful, all the related accounts will also be destroyed**.
### `GET /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Get a space email server configuration
### `POST /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Update an existing a space email server configuration.
JSON parameters:
* `host` **required**, the email server hostname
* `port` **required**, integer, the port
* `username`, the username
* `password`, the password
* `from_address`, email address, the sender email address
* `from_name`, the sender name
* `signature`, a text that will end every emails sent
### `DELETE /spaces/{domain}/email`
<span class="badge badge-error">Super Admin</span>
Delete the a space email server configuration.
## Account Creation Request Tokens
An `account_creation_request_token` is a unique token that can be validated and then used to generate a valid `account_creation_token`.
### `POST /account_creation_request_tokens`
<span class="badge badge-success">Public</span>
Create and return an `account_creation_request_token` that should then be validated to be used.
## Account Creation Tokens
An `account_creation_token` is a unique token that allow the creation or the validation of a unique account.
### `POST /account_creation_tokens/send-by-push`
<span class="badge badge-success">Public</span>
Create and send an `account_creation_token` using a push notification to the device.
Return `403` if a token was already sent, or if the tokens limit is reached for this device.
Return `503` if the token was not successfully sent.
JSON parameters:
* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm
* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters
* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes, underscore and colon characters
### `POST /account_creation_tokens/using-account-creation-request-token`
<span class="badge badge-success">Public</span>
Create an `account_creation_token` using an `account_creation_request_token`.
Return an `account_creation_token`.
Return `404` if the `account_creation_request_token` provided is not valid or expired otherwise.
JSON parameters:
* `account_creation_request_token` **required**
### `POST /account_creation_tokens/consume`
<span class="badge badge-info">User</span>
Consume an `account_creation_token` and link it to the authenticated account.
Return an `account_creation_token`.
Return `404` if the `account_creation_token` provided is not valid.
JSON parameters:
* `account_creation_token` **required**
### `POST /account_creation_tokens`
<span class="badge badge-warning">Admin</span>
Create and return an `account_creation_token`.
## Account Recovery Tokens
An `account_recovery_token` is a unique token that allow the recovery of an account.
It can be used on the following page that also accepts a `phone` optional parameter to prefil the recovery form:
{{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_']) }}
{{ route('account.recovery.show.phone', ['account_recovery_token' => '_the_token_', 'phone' => '+3312341234']) }}
### `POST /account_recovery_tokens/send-by-push`
<span class="badge badge-success">Public</span>
Create and send an `account_recovery_token` using a push notification to the device.
Return `403` if a token was already sent, or if the tokens limit is reached for this device.
Return `503` if the token was not successfully sent.
JSON parameters:
* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm
* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters
* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes, underscore and colon characters
## Auth Tokens
### `POST /accounts/auth_token`
<span class="badge badge-success">Public</span>
Generate an `auth_token`. To attach the generated token to an account see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach).
Return the `auth_token` object.
### `GET /accounts/auth_token/{auth_token}/attach`
<span class="badge badge-info">User</span>
Attach a publicly generated authentication token to the currently authenticated account.
Return `404` if the token is non existing or invalid.
## Accounts
### `POST /accounts/with-account-creation-token`
<span class="badge badge-success">Public</span>
Create an account using an `account_creation_token`.
Return `422` if the parameters are invalid or if the token is expired.
Return `403` if the `max_accounts` limit of the corresponding Space is reached.
JSON parameters:
* `username` unique username, minimum 6 characters
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `account_creation_token` the unique `account_creation_token`
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
### `GET /accounts/{sip}/info`
<span class="badge badge-success">Public</span>
Retrieve public information about the account.
Return `404` if the account doesn't exists.
### `GET /accounts/me/api_key/{auth_token}`
<span class="badge badge-success">Public</span>
Generate and retrieve a fresh API Key from an `auth_token`. The `auth_token` must be attached to an existing account, see [`auth_token` attachement endpoint](#get-accountsauthtokenauthtokenattach) to do so.
Return `404` if the token is invalid or not attached.
This endpoint is also setting the API Key as a Cookie.
### `GET /accounts/me/api_key`
<span class="badge badge-info">User</span>
Generate and retrieve a fresh API Key.
This endpoint is also setting the API Key as a Cookie.
### `GET /accounts/me`
<span class="badge badge-info">User</span>
Retrieve the account information.
### `GET /accounts/me/services/turn`
<span class="badge badge-info">User</span>
If configured, returns valid TURN credentials following the [draft-uberti-behave-turn-rest-00 IEFT Draft](https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00).
### `GET /accounts/me/provision`
<span class="badge badge-info">User</span>
Provision the account by generating a fresh `provisioning_token`.
Return the account object.
### `DELETE /accounts/me`
<span class="badge badge-info">User</span>
Delete the account.
### `POST /accounts/me/password`
<span class="badge badge-info">User</span>
Change the account password.
JSON parameters:
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `old_password` **required** if the password is already set, the old password
* `password` **required**, the new password
### `POST /accounts`
<span class="badge badge-warning">Admin</span>
To create an account directly from the API.
Return `403` if the `max_accounts` limit of the corresponding Space is reached.
JSON parameters:
* `username` unique username, minimum 6 characters
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `domain` **not configurable by default**, must exist in one of the configured Spaces. Only configurable if the admin is a super admin. Otherwise the SIP domain of the corresponding space is used.
* `activated` optional, a boolean, set to `false` by default
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default, create an admin account
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
### `PUT /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Update an existing account. Ensure to resend all the parameters to not reset them.
JSON parameters:
* `username` unique username, minimum 6 characters
* `domain` **not configurable by default**, must exist in one of the configured Spaces. Only configurable if the admin is a super admin. Otherwise the SIP domain of the corresponding space is used.
* `password` **required** minimum 6 characters
* `algorithm` **required**, values can be `SHA-256` or `MD5`
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
Using this endpoint you can also set a fresh dictionnary if the parameter is set. The existing dictionary entries will be destroyed.
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
This endpoint also return the current `phone_change_code` and `email_change_code` if they are available.
### `GET /accounts`
<span class="badge badge-warning">Admin</span>
Retrieve all the accounts, paginated.
### `GET /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Retrieve a specific account.
### `GET /accounts/{sip}/search`
<span class="badge badge-warning">Admin</span>
Search for a specific account by sip address.
### `GET /accounts/{email}/search-by-email`
<span class="badge badge-warning">Admin</span>
Search for a specific account by email.
### `DELETE /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Delete a specific account and its related information.
### `POST /accounts/{id}/activate`
<span class="badge badge-warning">Admin</span>
Activate an account.
### `POST /accounts/{id}/deactivate`
<span class="badge badge-warning">Admin</span>
Deactivate an account.
### `POST /accounts/{id}/block`
<span class="badge badge-warning">Admin</span>
Block an account.
### `POST /accounts/{id}/unblock`
<span class="badge badge-warning">Admin</span>
Unblock an account.
### `GET /accounts/{id}/provision`
<span class="badge badge-warning">Admin</span>
Provision an account by generating a fresh `provisioning_token`.
### `POST /accounts/{id}/send_provisioning_email`
<span class="badge badge-warning">Admin</span>
Send a provisioning email to the account.
### `POST /accounts/{id}/send_reset_password_email`
<span class="badge badge-warning">Admin</span>
Send a password reset email to the account.
## Accounts email
### `POST /accounts/me/email/request`
<span class="badge badge-info">User</span>
Request to change the account email. An email will be sent to the new email address to confirm the operation.
Will return `403` if the account doesn't have a validated <a href='#account-creation-tokens'>Account Creation Token</a> attached to it.
JSON parameters:
* `email` the new email address, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
### `POST /accounts/me/email`
<span class="badge badge-info">User</span>
Confirm the code received and change the email.
Activate the account.
JSON parameters:
* `code` the code received by email
Return the updated account.
## Accounts phone number
### `POST /accounts/me/phone/request`
<span class="badge badge-info">User</span>
Request a specific code by SMS to change the phone number.
Will return `403` if the account doesn't have a validated <a href='#account-creation-tokens'>Account Creation Token</a> attached to it.
JSON parameters:
* `phone` the phone number to send the SMS
### `POST /accounts/me/phone`
<span class="badge badge-info">User</span>
Confirm the code received and change the phone number.
Activate the account.
JSON parameters:
* `code` the received SMS code
Return the updated account.
## Accounts devices
### `GET /accounts/{id/me}/devices`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return the user registered devices.
### `DELETE /accounts/{id/me}/devices/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Remove one of the user registered devices.
## Account contacts
### `GET /accounts/me/contacts`
<span class="badge badge-info">User</span>
Return the user contacts.
### `GET /accounts/me/contacts/{sip}`
<span class="badge badge-info">User</span>
Return a user contact.
## vCards storage
### `POST /accounts/{id/me}/vcards-storage`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Store a vCard.
JSON parameters:
* `vcard`, mandatory, a valid vCard having a mandatory `UID` parameter that is uniquelly identifying it. This `UID` parameter will then be used to manipulate the vcard through the following endpoints as `uuid`.
### `PUT /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Update a vCard.
JSON parameters:
* `vcard`, mandatory, a valid vCard having a mandatory `UID` parameter that is uniquelly identifying it and is the same as the `uuid` parameter.
### `GET /accounts/{id/me}/vcards-storage`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return the list of stored vCards
### `GET /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Return a stored vCard
### `DELETE /accounts/{id/me}/vcards-storage/{uuid}`
<span class="badge badge-warning">Admin</span>
<span class="badge badge-info">User</span>
Delete a stored vCard
## Contacts
### `GET /accounts/{id}/contacts`
<span class="badge badge-warning">Admin</span>
Get all the account contacts.
### `POST /accounts/{id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Add a contact to the list.
### `DELETE /accounts/{id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Remove a contact from the list.
## Dictionary
### `GET /accounts/{id}/dictionary`
<span class="badge badge-warning">Admin</span>
Get all the account dictionary entries.
### `POST /accounts/{id}/dictionary/{key}`
<span class="badge badge-warning">Admin</span>
Add or update a new entry to the dictionary
JSON parameters:
* `value` **required**, the entry value
## External Account
### `GET /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Get the external account.
### `POST /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Create or update the external account.
JSON parameters:
* `username` **required**
* `domain` **required**
* `password` **required**
* `realm` must be different than `domain`
* `registrar` must be different than `domain`
* `outbound_proxy` must be different than `domain`
* `protocol` **required**, must be `UDP`, `TCP` or `TLS`
### `DELETE /accounts/{id}/external`
<span class="badge badge-warning">Admin</span>
Delete the external account.
## Account Actions
The following endpoints will return `403 Forbidden` if the requested account doesn't have a DTMF protocol configured.
### `GET /accounts/{id}/actions`
<span class="badge badge-warning">Admin</span>
Show an account related actions.
### `GET /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Show an account related action.
### `POST /accounts/{id}/actions/`
<span class="badge badge-warning">Admin</span>
Create an account action.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
* `code` **required**, alpha numeric, lowercase
### `PUT /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Create an account action.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
* `code` **required**, alpha numeric, lowercase
### `DELETE /accounts/{id}/actions/{action_id}`
<span class="badge badge-warning">Admin</span>
Delete an account related action.
## Contacts Lists
### `GET /contacts_lists`
<span class="badge badge-warning">Admin</span>
Show all the contacts lists.
### `GET /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Show a contacts list.
### `POST /contacts_lists`
<span class="badge badge-warning">Admin</span>
Create a contacts list.
JSON parameters:
* `title` **required**
* `description` **required**
### `PUT /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Update a contacts list.
JSON parameters:
* `title` **required**
* `description` **required**
### `DELETE /contacts_lists/{id}`
<span class="badge badge-warning">Admin</span>
Delete a contacts list.
### `POST /contacts_lists/{contacts_list_id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Add a contact to the contacts list.
### `DELETE /contacts_lists/{contacts_list_id}/contacts/{contact_id}`
<span class="badge badge-warning">Admin</span>
Remove a contact from the contacts list.
### `POST /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Add a contacts list to the account.
### `DELETE /accounts/{id}/contacts_lists/{contacts_list_id}`
<span class="badge badge-warning">Admin</span>
Remove a contacts list from the account.
## Account Types
### `GET /account_types`
<span class="badge badge-warning">Admin</span>
Show all the account types.
### `GET /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Show an account type.
### `POST /account_types`
<span class="badge badge-warning">Admin</span>
Create an account type.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
### `PUT /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Update an account type.
JSON parameters:
* `key` **required**, alpha numeric with dashes, lowercase
### `DELETE /account_types/{id}`
<span class="badge badge-warning">Admin</span>
Delete an account type.
### `POST /accounts/{id}/types/{type_id}`
<span class="badge badge-warning">Admin</span>
Add a type to the account.
### `DELETE /accounts/{id}/contacts/{type_id}`
<span class="badge badge-warning">Admin</span>
Remove a type from the account.
## Messages ## Messages
@ -865,63 +77,8 @@ Return the list of Phone Countries and their current status.
If a country is deactivated all the new submitted phones submitted on the platform will be blocked. If a country is deactivated all the new submitted phones submitted on the platform will be blocked.
## Statistics
FlexiAPI can record logs generated by the FlexiSIP server and compile them into statistics. @include('api.documentation.statistics')
### `POST /statistics/messages`
<span class="badge badge-warning">Admin</span>
Announce the creation of a message.
JSON parameters:
* `id` **required**, string
* `from` **required**, string the sender of the message
* `sent_at` **required**, string, format ISO8601, when the message was actually sent
* `encrypted` **required**, boolean
* `conference_id` string
### `PATCH /statistics/messages/{message_id}/to/{to}/devices/{device_id}`
<span class="badge badge-warning">Admin</span>
Complete a message status.
JSON parameters:
* `last_status` **required**, an integer containing the last status code
* `received_at` **required**, format ISO8601, when the message was received
### `POST /statistics/calls`
<span class="badge badge-warning">Admin</span>
Announce the beginning of a call.
JSON parameters:
* `id` **required**, string
* `from` **required**, string the initier of the call
* `to` **required**, string the destination of the call
* `initiated_at` **required**, string, format ISO8601, when the call was started
* `ended_at` string, format ISO8601, when the call finished
* `conference_id` string
### `PATCH /statistics/calls/{call_id}/devices/{device_id}`
<span class="badge badge-warning">Admin</span>
Complete a call status.
JSON parameters:
* `rang_at` format ISO8601, when the device rang
* `invite_terminated`
* `at` format ISO8601, when the invitation ended
* `state` the termination state
### `PATCH /statistics/calls/{call_id}`
<span class="badge badge-warning">Admin</span>
Update a call when ending.
JSON parameters: JSON parameters:

View file

@ -18,18 +18,20 @@
*/ */
use App\Http\Controllers\Api\Account\VcardsStorageController; use App\Http\Controllers\Api\Account\VcardsStorageController;
use App\Http\Controllers\Api\Admin\AccountActionController; use App\Http\Controllers\Api\Admin\Account\ActionController;
use App\Http\Controllers\Api\Admin\AccountContactController; use App\Http\Controllers\Api\Admin\Account\CardDavCredentialsController;
use App\Http\Controllers\Api\Admin\Account\ContactController;
use App\Http\Controllers\Api\Admin\Account\DictionaryController;
use App\Http\Controllers\Api\Admin\Account\TypeController;
use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController; use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController;
use App\Http\Controllers\Api\Admin\AccountDictionaryController;
use App\Http\Controllers\Api\Admin\AccountTypeController;
use App\Http\Controllers\Api\Admin\ContactsListController; use App\Http\Controllers\Api\Admin\ContactsListController;
use App\Http\Controllers\Api\Admin\ExternalAccountController; use App\Http\Controllers\Api\Admin\ExternalAccountController;
use App\Http\Controllers\Api\Admin\Space\CardDavServerController;
use App\Http\Controllers\Api\Admin\Space\EmailServerController;
use App\Http\Controllers\Api\Admin\SpaceController; use App\Http\Controllers\Api\Admin\SpaceController;
use App\Http\Controllers\Api\Admin\EmailServerController;
use App\Http\Controllers\Api\Admin\VcardsStorageController as AdminVcardsStorageController; use App\Http\Controllers\Api\Admin\VcardsStorageController as AdminVcardsStorageController;
use App\Http\Controllers\Api\StatisticsMessageController;
use App\Http\Controllers\Api\StatisticsCallController; use App\Http\Controllers\Api\StatisticsCallController;
use App\Http\Controllers\Api\StatisticsMessageController;
use Illuminate\Http\Request; use Illuminate\Http\Request;
Route::get('/', 'Api\ApiController@documentation')->name('api'); Route::get('/', 'Api\ApiController@documentation')->name('api');
@ -93,23 +95,19 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
// Super admin // Super admin
Route::group(['middleware' => ['auth.super_admin']], function () { Route::group(['middleware' => ['auth.super_admin']], function () {
Route::prefix('spaces')->controller(SpaceController::class)->group(function () { Route::apiResource('spaces', SpaceController::class);
Route::get('/', 'index');
Route::get('{domain}', 'show');
Route::post('/', 'store');
Route::put('{domain}', 'update');
Route::delete('{domain}', 'destroy');
});
Route::prefix('spaces/{domain}/email')->controller(EmailServerController::class)->group(function () { Route::prefix('spaces/{domain}/email')->controller(EmailServerController::class)->group(function () {
Route::get('/', 'show'); Route::get('/', 'show');
Route::post('/', 'store'); Route::post('/', 'store');
Route::delete('/', 'destroy'); Route::delete('/', 'destroy');
}); });
Route::apiResource('spaces/{domain}/carddavs', CardDavServerController::class);
}); });
// Account creation token // Account creation token
Route::post('account_creation_tokens', 'Api\Admin\AccountCreationTokenController@create'); Route::post('account_creation_tokens', 'Api\Admin\Account\CreationTokenController@create');
// Accounts // Accounts
Route::prefix('accounts')->controller(AdminAccountController::class)->group(function () { Route::prefix('accounts')->controller(AdminAccountController::class)->group(function () {
@ -140,15 +138,19 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
}); });
// Account contacts // Account contacts
Route::prefix('accounts/{id}/contacts')->controller(AccountContactController::class)->group(function () { Route::prefix('accounts/{id}/contacts')->controller(ContactController::class)->group(function () {
Route::get('/', 'index'); Route::get('/', 'index');
Route::get('{contact_id}', 'show'); Route::get('{contact_id}', 'show');
Route::post('{contact_id}', 'add'); Route::post('{contact_id}', 'add');
Route::delete('{contact_id}', 'remove'); Route::delete('{contact_id}', 'remove');
}); });
Route::apiResource('accounts/{id}/actions', AccountActionController::class); Route::group(['middleware' => ['feature.carddav_user_credentials']], function () {
Route::apiResource('account_types', AccountTypeController::class); Route::apiResource('accounts/{id}/carddavs', CardDavCredentialsController::class, ['only' => ['index', 'show', 'update', 'destroy']]);
});
Route::apiResource('accounts/{id}/actions', ActionController::class);
Route::apiResource('account_types', TypeController::class);
Route::apiResource('accounts/{account_id}/vcards-storage', AdminVcardsStorageController::class); Route::apiResource('accounts/{account_id}/vcards-storage', AdminVcardsStorageController::class);
Route::apiResource('contacts_lists', ContactsListController::class); Route::apiResource('contacts_lists', ContactsListController::class);
@ -157,7 +159,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
Route::delete('{id}/contacts/{contacts_id}', 'contactRemove'); Route::delete('{id}/contacts/{contacts_id}', 'contactRemove');
}); });
Route::prefix('accounts/{id}/dictionary')->controller(AccountDictionaryController::class)->group(function () { Route::prefix('accounts/{id}/dictionary')->controller(DictionaryController::class)->group(function () {
Route::get('/', 'index'); Route::get('/', 'index');
Route::get('{key}', 'show'); Route::get('{key}', 'show');
Route::post('{key}', 'set'); Route::post('{key}', 'set');

View file

@ -26,16 +26,17 @@ use App\Http\Controllers\Account\PasswordController;
use App\Http\Controllers\Account\PhoneController; use App\Http\Controllers\Account\PhoneController;
use App\Http\Controllers\Account\ProvisioningController; use App\Http\Controllers\Account\ProvisioningController;
use App\Http\Controllers\Account\RecoveryController; use App\Http\Controllers\Account\RecoveryController;
use App\Http\Controllers\Admin\AccountAccountTypeController;
use App\Http\Controllers\Admin\AccountActionController;
use App\Http\Controllers\Admin\AccountActivityController;
use App\Http\Controllers\Admin\AccountContactController;
use App\Http\Controllers\Admin\AccountController as AdminAccountController; use App\Http\Controllers\Admin\AccountController as AdminAccountController;
use App\Http\Controllers\Admin\AccountDeviceController; use App\Http\Controllers\Admin\Account\AccountTypeController;
use App\Http\Controllers\Admin\AccountDictionaryController; use App\Http\Controllers\Admin\Account\ActionController;
use App\Http\Controllers\Admin\AccountImportController; use App\Http\Controllers\Admin\Account\ActivityController;
use App\Http\Controllers\Admin\AccountStatisticsController; use App\Http\Controllers\Admin\Account\CardDavCredentialsController;
use App\Http\Controllers\Admin\AccountTypeController; use App\Http\Controllers\Admin\Account\ContactController;
use App\Http\Controllers\Admin\Account\DeviceController as AdminAccountDeviceController;
use App\Http\Controllers\Admin\Account\DictionaryController;
use App\Http\Controllers\Admin\Account\ImportController;
use App\Http\Controllers\Admin\Account\StatisticsController as AdminAccountStatisticsController;
use App\Http\Controllers\Admin\Account\TypeController;
use App\Http\Controllers\Admin\ApiKeyController as AdminApiKeyController; use App\Http\Controllers\Admin\ApiKeyController as AdminApiKeyController;
use App\Http\Controllers\Admin\ContactsListContactController; use App\Http\Controllers\Admin\ContactsListContactController;
use App\Http\Controllers\Admin\ContactsListController; use App\Http\Controllers\Admin\ContactsListController;
@ -44,6 +45,7 @@ use App\Http\Controllers\Admin\PhoneCountryController;
use App\Http\Controllers\Admin\ProvisioningEmailController; use App\Http\Controllers\Admin\ProvisioningEmailController;
use App\Http\Controllers\Admin\ResetPasswordEmailController; use App\Http\Controllers\Admin\ResetPasswordEmailController;
use App\Http\Controllers\Admin\Space\EmailServerController; use App\Http\Controllers\Admin\Space\EmailServerController;
use App\Http\Controllers\Admin\Space\CardDavServerController;
use App\Http\Controllers\Admin\SpaceController; use App\Http\Controllers\Admin\SpaceController;
use App\Http\Controllers\Admin\StatisticsController; use App\Http\Controllers\Admin\StatisticsController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -51,7 +53,7 @@ use Illuminate\Support\Facades\Route;
Route::redirect('/', 'login')->name('account.home'); Route::redirect('/', 'login')->name('account.home');
Route::get('about', 'AboutController@about')->name('about'); Route::get('about', 'AboutController@about')->name('about');
Route::middleware(['web_panel_enabled'])->group(function () { Route::middleware(['feature.web_panel_enabled'])->group(function () {
Route::get('wizard/{provisioning_token}', 'Account\ProvisioningController@wizard')->name('provisioning.wizard'); Route::get('wizard/{provisioning_token}', 'Account\ProvisioningController@wizard')->name('provisioning.wizard');
Route::get('login', 'Account\AuthenticateController@login')->name('account.login'); Route::get('login', 'Account\AuthenticateController@login')->name('account.login');
@ -88,11 +90,11 @@ Route::name('provisioning.')->prefix('provisioning')->controller(ProvisioningCon
Route::get('/', 'show')->name('show'); Route::get('/', 'show')->name('show');
}); });
Route::middleware(['web_panel_enabled'])->group(function () { Route::middleware(['feature.web_panel_enabled'])->group(function () {
Route::middleware(['public_registration'])->group(function () { Route::middleware(['feature.public_registration'])->group(function () {
Route::redirect('register', 'register/email')->name('account.register'); Route::redirect('register', 'register/email')->name('account.register');
Route::middleware(['phone_registration'])->group(function () { Route::middleware(['feature.phone_registration'])->group(function () {
Route::get('register/phone', 'Account\RegisterController@registerPhone')->name('account.register.phone'); Route::get('register/phone', 'Account\RegisterController@registerPhone')->name('account.register.phone');
}); });
@ -117,7 +119,7 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::post('/', 'store')->name('email.update'); Route::post('/', 'store')->name('email.update');
}); });
Route::middleware(['phone_registration'])->group(function () { Route::middleware(['feature.phone_registration'])->group(function () {
Route::prefix('phone')->controller(PhoneController::class)->group(function () { Route::prefix('phone')->controller(PhoneController::class)->group(function () {
Route::get('change', 'change')->name('phone.change'); Route::get('change', 'change')->name('phone.change');
Route::post('change', 'requestChange')->name('phone.request_change'); Route::post('change', 'requestChange')->name('phone.request_change');
@ -169,6 +171,8 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::get('delete', 'delete')->name('delete'); Route::get('delete', 'delete')->name('delete');
Route::delete('/', 'destroy')->name('destroy'); Route::delete('/', 'destroy')->name('destroy');
}); });
Route::resource('{space}/carddavs', CardDavServerController::class, ['except' => ['index', 'show']]);
Route::get('{space}/carddavs/{carddav}/delete', 'Admin\Space\CardDavServerController@delete')->name('carddavs.delete');
}); });
Route::name('api_keys.')->prefix('api_keys')->controller(AdminApiKeyController::class)->group(function () { Route::name('api_keys.')->prefix('api_keys')->controller(AdminApiKeyController::class)->group(function () {
@ -205,14 +209,14 @@ Route::middleware(['web_panel_enabled'])->group(function () {
}); });
Route::name('account.')->prefix('accounts')->group(function () { Route::name('account.')->prefix('accounts')->group(function () {
Route::name('import.')->prefix('import')->controller(AccountImportController::class)->group(function () { Route::name('import.')->prefix('import')->controller(ImportController::class)->group(function () {
Route::get('/', 'create')->name('create'); Route::get('/', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
Route::post('handle', 'handle')->name('handle'); Route::post('handle', 'handle')->name('handle');
}); });
Route::middleware(['intercom_features'])->group(function () { Route::middleware(['feature.intercom'])->group(function () {
Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () { Route::name('type.')->prefix('types')->controller(TypeController::class)->group(function () {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->name('index');
Route::get('create', 'create')->name('create'); Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
@ -222,13 +226,13 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::delete('{type_id}', 'destroy')->name('destroy'); Route::delete('{type_id}', 'destroy')->name('destroy');
}); });
Route::name('account_type.')->prefix('{account}/types')->controller(AccountAccountTypeController::class)->group(function () { Route::name('account_type.')->prefix('{account}/types')->controller(AccountTypeController::class)->group(function () {
Route::get('create', 'create')->name('create'); Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
Route::delete('{type_id}', 'destroy')->name('destroy'); Route::delete('{type_id}', 'destroy')->name('destroy');
}); });
Route::name('action.')->prefix('{account}/actions')->controller(AccountActionController::class)->group(function () { Route::name('action.')->prefix('{account}/actions')->controller(ActionController::class)->group(function () {
Route::get('create', 'create')->name('create'); Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
Route::get('{action_id}/edit', 'edit')->name('edit'); Route::get('{action_id}/edit', 'edit')->name('edit');
@ -268,7 +272,7 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::get('send', 'send')->name('send'); Route::get('send', 'send')->name('send');
}); });
Route::name('contact.')->prefix('{account}/contacts')->controller(AccountContactController::class)->group(function () { Route::name('contact.')->prefix('{account}/contacts')->controller(ContactController::class)->group(function () {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->name('index');
Route::get('create', 'create')->name('create'); Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
@ -276,12 +280,15 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::delete('/', 'destroy')->name('destroy'); Route::delete('/', 'destroy')->name('destroy');
}); });
Route::name('device.')->prefix('{account}/devices')->controller(AccountDeviceController::class)->group(function () { Route::name('device.')->prefix('{account}/devices')->controller(AdminAccountDeviceController::class)->group(function () {
Route::get('{device_id}/delete', 'delete')->name('delete'); Route::get('{device_id}/delete', 'delete')->name('delete');
Route::delete('/', 'destroy')->name('destroy'); Route::delete('/', 'destroy')->name('destroy');
}); });
Route::name('dictionary.')->prefix('{account}/dictionary')->controller(AccountDictionaryController::class)->group(function () { Route::resource('{account}/carddavs', CardDavCredentialsController::class, ['only' => ['create', 'store', 'destroy']]);
Route::get('{account}/carddavs/{carddav}/delete', 'Admin\Account\CardDavCredentialsController@delete')->name('carddavs.delete');
Route::name('dictionary.')->prefix('{account}/dictionary')->controller(DictionaryController::class)->group(function () {
Route::get('create', 'create')->name('create'); Route::get('create', 'create')->name('create');
Route::post('/', 'store')->name('store'); Route::post('/', 'store')->name('store');
Route::get('{entry}/edit', 'edit')->name('edit'); Route::get('{entry}/edit', 'edit')->name('edit');
@ -297,11 +304,11 @@ Route::middleware(['web_panel_enabled'])->group(function () {
Route::delete('/', 'destroy')->name('destroy'); Route::delete('/', 'destroy')->name('destroy');
}); });
Route::name('activity.')->prefix('{account}/activity')->controller(AccountActivityController::class)->group(function () { Route::name('activity.')->prefix('{account}/activity')->controller(ActivityController::class)->group(function () {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->name('index');
}); });
Route::name('statistics.')->prefix('{account}/statistics')->controller(AccountStatisticsController::class)->group(function () { Route::name('statistics.')->prefix('{account}/statistics')->controller(AdminAccountStatisticsController::class)->group(function () {
Route::get('/', 'show')->name('show'); Route::get('/', 'show')->name('show');
Route::post('call_logs', 'editCallLogs')->name('edit_call_logs'); Route::post('call_logs', 'editCallLogs')->name('edit_call_logs');
Route::get('call_logs', 'showCallLogs')->name('show_call_logs'); Route::get('call_logs', 'showCallLogs')->name('show_call_logs');

Some files were not shown because too many files have changed in this diff Show more