mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-04-17 19:58:27 +00:00
Fix FLEXIAPI-433 Implement CallForwarding
This commit is contained in:
parent
126f25de5a
commit
3f0ecc297b
33 changed files with 1388 additions and 596 deletions
|
|
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
||||||
- **Rockylinux 10 support** Packages are now available in the official repository
|
- **Rockylinux 10 support** Packages are now available in the official repository
|
||||||
- **Artisan cleanup script for statistics** Add an artisan console script to clear statistics after n days `app:clear-statistics {days} {--apply}`
|
- **Artisan cleanup script for statistics** Add an artisan console script to clear statistics after n days `app:clear-statistics {days} {--apply}`
|
||||||
- **Add Voicemail features and related API endpoints** to integrate with `flexisip-voicemail`
|
- **Add Voicemail features and related API endpoints** to integrate with `flexisip-voicemail`
|
||||||
|
- **Add Call Forwarding features and related API endpoints**
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,20 @@ class Account extends Authenticatable
|
||||||
return $this->hasMany(AccountFile::class)->latest();
|
return $this->hasMany(AccountFile::class)->latest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function callForwardings()
|
||||||
|
{
|
||||||
|
return $this->hasMany(CallForwarding::class)->latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCallForwardingsDefaultAttribute()
|
||||||
|
{
|
||||||
|
$callForwardings = $this->callForwardings->keyBy('type');
|
||||||
|
$resolved['always'] = $callForwardings['always'] ?? new CallForwarding(['type' => 'always']);
|
||||||
|
$resolved['away'] = $callForwardings['away'] ?? new CallForwarding(['type' => 'away']);
|
||||||
|
$resolved['busy'] = $callForwardings['busy'] ?? new CallForwarding(['type' => 'busy']);
|
||||||
|
return $resolved;
|
||||||
|
}
|
||||||
|
|
||||||
public function voicemails()
|
public function voicemails()
|
||||||
{
|
{
|
||||||
return $this->hasMany(AccountFile::class)
|
return $this->hasMany(AccountFile::class)
|
||||||
|
|
|
||||||
19
flexiapi/app/CallForwarding.php
Normal file
19
flexiapi/app/CallForwarding.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CallForwarding extends Model
|
||||||
|
{
|
||||||
|
protected $casts = [
|
||||||
|
'enabled' => 'boolean'
|
||||||
|
];
|
||||||
|
protected $fillable = ['enabled', 'account_id', 'type', 'forward_to', 'sip_uri'];
|
||||||
|
protected $hidden = ['account_id'];
|
||||||
|
|
||||||
|
public function account()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Account::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -175,21 +175,21 @@ function captchaConfigured(): bool
|
||||||
return env('HCAPTCHA_SECRET', false) != false || env('HCAPTCHA_SITEKEY', false) != false;
|
return env('HCAPTCHA_SECRET', false) != false || env('HCAPTCHA_SITEKEY', false) != false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveUserContacts(Request $request)
|
function resolveUserContacts(Account $account)
|
||||||
{
|
{
|
||||||
$selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol', 'display_name'];
|
$selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol', 'display_name'];
|
||||||
|
|
||||||
return Account::withoutGlobalScopes()->whereIn('id', function ($query) use ($request) {
|
return Account::withoutGlobalScopes()->whereIn('id', function ($query) use ($account) {
|
||||||
$query->select('contact_id')
|
$query->select('contact_id')
|
||||||
->from('contacts')
|
->from('contacts')
|
||||||
->where('account_id', $request->user()->id)
|
->where('account_id', $account->id)
|
||||||
->union(
|
->union(
|
||||||
DB::table('contacts_list_contact')
|
DB::table('contacts_list_contact')
|
||||||
->select('contact_id')
|
->select('contact_id')
|
||||||
->whereIn('contacts_list_id', function ($query) use ($request) {
|
->whereIn('contacts_list_id', function ($query) use ($account) {
|
||||||
$query->select('contacts_list_id')
|
$query->select('contacts_list_id')
|
||||||
->from('account_contacts_list')
|
->from('account_contacts_list')
|
||||||
->where('account_id', $request->user()->id);
|
->where('account_id', $account->id);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})->select($selected);
|
})->select($selected);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class ContactVcardController extends Controller
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
return response(
|
return response(
|
||||||
resolveUserContacts($request)->get()->map(function ($contact) {
|
resolveUserContacts($request->user())->get()->map(function ($contact) {
|
||||||
return $contact->toVcard4();
|
return $contact->toVcard4();
|
||||||
})->implode("\n")
|
})->implode("\n")
|
||||||
);
|
);
|
||||||
|
|
@ -36,7 +36,7 @@ class ContactVcardController extends Controller
|
||||||
|
|
||||||
public function show(Request $request, string $sip)
|
public function show(Request $request, string $sip)
|
||||||
{
|
{
|
||||||
return resolveUserContacts($request)
|
return resolveUserContacts($request->user())
|
||||||
->sip($sip)
|
->sip($sip)
|
||||||
->firstOrFail()
|
->firstOrFail()
|
||||||
->toVcard4();
|
->toVcard4();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Account;
|
||||||
|
|
||||||
|
use App\Account;
|
||||||
|
use App\CallForwarding;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CallForwardingController extends Controller
|
||||||
|
{
|
||||||
|
public function update(Request $request, int $accountId)
|
||||||
|
{
|
||||||
|
$account = Account::findOrFail($accountId);
|
||||||
|
$contactsIds = resolveUserContacts($account)->pluck('id')->toArray();
|
||||||
|
|
||||||
|
$forwardTo = 'required|in:sip_uri,contact,voicemail';
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'always.forward_to' => $forwardTo,
|
||||||
|
'always.sip_uri' => 'nullable|starts_with:sip:|required_if:always.forward_to,sip_uri',
|
||||||
|
'always.contact_id' => ['required_if:always.forward_to,contact', Rule::in($contactsIds)],
|
||||||
|
|
||||||
|
'away.forward_to' => $forwardTo,
|
||||||
|
'away.sip_uri' => 'nullable|starts_with:sip:|required_if:away.forward_to,sip_uri',
|
||||||
|
'away.contact_id' => ['required_if:away.forward_to,contact', Rule::in($contactsIds)],
|
||||||
|
|
||||||
|
'busy.forward_to' => $forwardTo,
|
||||||
|
'busy.sip_uri' => 'nullable|starts_with:sip:|required_if:busy.forward_to,sip_uri',
|
||||||
|
'busy.contact_id' => ['required_if:busy.forward_to,contact', Rule::in($contactsIds)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$account->callForwardings()->update(['enabled' => false]);
|
||||||
|
|
||||||
|
if (array_key_exists('enabled', $request->get('always'))) {
|
||||||
|
$alwaysForwarding = $account->callForwardings()->where('type', 'always')->first() ?? new CallForwarding;
|
||||||
|
$alwaysForwarding->enabled = true;
|
||||||
|
$alwaysForwarding->account_id = $account->id;
|
||||||
|
$alwaysForwarding->type = 'always';
|
||||||
|
$alwaysForwarding->forward_to = $request->get('always')['forward_to'];
|
||||||
|
$alwaysForwarding->sip_uri = $request->get('always')['sip_uri'];
|
||||||
|
$alwaysForwarding->contact_id = $request->get('always')['forward_to'] == 'contact'
|
||||||
|
? $request->get('always')['contact_id']
|
||||||
|
: null;
|
||||||
|
$alwaysForwarding->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('enabled', $request->get('away'))) {
|
||||||
|
$awayForwarding = $account->callForwardings()->where('type', 'away')->first() ?? new CallForwarding;
|
||||||
|
$awayForwarding->enabled = true;
|
||||||
|
$awayForwarding->account_id = $account->id;
|
||||||
|
$awayForwarding->type = 'away';
|
||||||
|
$awayForwarding->forward_to = $request->get('away')['forward_to'];
|
||||||
|
$awayForwarding->sip_uri = $request->get('away')['sip_uri'];
|
||||||
|
$awayForwarding->contact_id = $request->get('away')['forward_to'] == 'contact'
|
||||||
|
? $request->get('away')['contact_id']
|
||||||
|
: null;
|
||||||
|
$awayForwarding->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('enabled', $request->get('busy'))) {
|
||||||
|
$busyForwarding = $account->callForwardings()->where('type', 'busy')->first() ?? new CallForwarding;
|
||||||
|
$busyForwarding->enabled = true;
|
||||||
|
$busyForwarding->account_id = $account->id;
|
||||||
|
$busyForwarding->type = 'busy';
|
||||||
|
$busyForwarding->forward_to = $request->get('busy')['forward_to'];
|
||||||
|
$busyForwarding->sip_uri = $request->get('busy')['sip_uri'];
|
||||||
|
$busyForwarding->contact_id = $request->get('busy')['forward_to'] == 'contact'
|
||||||
|
? $request->get('busy')['contact_id']
|
||||||
|
: null;
|
||||||
|
$busyForwarding->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.account.telephony.show', $account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ class ContactController extends Controller
|
||||||
|
|
||||||
return view('admin.account.contact.index', [
|
return view('admin.account.contact.index', [
|
||||||
'account' => $account,
|
'account' => $account,
|
||||||
'contacts_lists' => ContactsList::whereNotIn('id', function ($query) use ($accountId) {
|
'contacts_lists' => $account->space->contactsLists()->whereNotIn('id', function ($query) use ($accountId) {
|
||||||
$query->select('contacts_list_id')
|
$query->select('contacts_list_id')
|
||||||
->from('account_contacts_list')
|
->from('account_contacts_list')
|
||||||
->where('account_id', $accountId);
|
->where('account_id', $accountId);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Account;
|
||||||
|
|
||||||
|
use App\Account;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TelephonyController extends Controller
|
||||||
|
{
|
||||||
|
public function show(int $accountId)
|
||||||
|
{
|
||||||
|
return view('admin.account.telephony.show', [
|
||||||
|
'account' => Account::findOrFail($accountId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Account;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use \App\Http\Controllers\Api\Admin\Account\CallForwardingController as AdminCallForwardingController;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CallForwardingController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return (new AdminCallForwardingController)->index($request, $request->user()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
return (new AdminCallForwardingController)->store($request, $request->user()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
return (new AdminCallForwardingController)->update($request, $request->user()->id, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, string $id)
|
||||||
|
{
|
||||||
|
return (new AdminCallForwardingController)->show($request, $request->user()->id, $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, string $id)
|
||||||
|
{
|
||||||
|
return (new AdminCallForwardingController)->destroy($request, $request->user()->id, $id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,11 +26,11 @@ class ContactController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
return resolveUserContacts($request)->get();
|
return resolveUserContacts($request->user())->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Request $request, string $sip)
|
public function show(Request $request, string $sip)
|
||||||
{
|
{
|
||||||
return resolveUserContacts($request)->sip($sip)->firstOrFail();
|
return resolveUserContacts($request->user())->sip($sip)->firstOrFail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Admin\Account;
|
||||||
|
|
||||||
|
use App\Account;
|
||||||
|
use App\CallForwarding;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Rules\CallForwardingEnable;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CallForwardingController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request, int $accountId)
|
||||||
|
{
|
||||||
|
return Account::findOrFail($accountId)->callForwardings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, int $accountId)
|
||||||
|
{
|
||||||
|
$account = Account::findOrFail($accountId);
|
||||||
|
$request->validate([
|
||||||
|
'type' => [
|
||||||
|
'required',
|
||||||
|
'in:always,away,busy',
|
||||||
|
Rule::unique('call_forwardings', 'type')->where(fn($query) => $query->where('account_id', $accountId))
|
||||||
|
],
|
||||||
|
'forward_to' => 'required|in:sip_uri,contact,voicemail',
|
||||||
|
'sip_uri' => 'nullable|starts_with:sip:|required_if:forward_to,sip_uri',
|
||||||
|
'enabled' => ['required', 'boolean', new CallForwardingEnable($request, $account)],
|
||||||
|
'contact_id' => ['required_if:forward_to,contact', Rule::in(resolveUserContacts($account)->pluck('id')->toArray())],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$callForwarding = new CallForwarding;
|
||||||
|
$callForwarding->account_id = $account->id;
|
||||||
|
$callForwarding->type = $request->get('type');
|
||||||
|
$callForwarding->forward_to = $request->get('forward_to');
|
||||||
|
$callForwarding->sip_uri = $request->get('sip_uri');
|
||||||
|
$callForwarding->enabled = $request->get('enabled');
|
||||||
|
$callForwarding->contact_id = $request->get('contact_id');
|
||||||
|
$callForwarding->save();
|
||||||
|
|
||||||
|
return $callForwarding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $accountId, string $id)
|
||||||
|
{
|
||||||
|
$account = Account::findOrFail($accountId);
|
||||||
|
$callForwarding = $account->callForwardings()->where('id', $id)->firstOrFail();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'type' => [
|
||||||
|
'required',
|
||||||
|
'in:always,away,busy',
|
||||||
|
Rule::unique('call_forwardings', 'type')
|
||||||
|
->where(fn($query) => $query->where('account_id', $accountId))
|
||||||
|
->ignore($callForwarding->id)
|
||||||
|
],
|
||||||
|
'forward_to' => 'required|in:sip_uri',
|
||||||
|
'sip_uri' => 'required|starts_with:sip',
|
||||||
|
'enabled' => ['required', 'boolean', new CallForwardingEnable($request, $account)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$callForwarding->forward_to = $request->get('forward_to');
|
||||||
|
$callForwarding->sip_uri = $request->get('sip_uri');
|
||||||
|
$callForwarding->enabled = $request->get('enabled');
|
||||||
|
$callForwarding->save();
|
||||||
|
|
||||||
|
return $callForwarding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $accountId, string $id)
|
||||||
|
{
|
||||||
|
return Account::findOrFail($accountId)->callForwardings()->where('id', $id)->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $accountId, string $id)
|
||||||
|
{
|
||||||
|
return Account::findOrFail($accountId)->callForwardings()->where('id', $id)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ class AccountController extends Controller
|
||||||
|
|
||||||
public function search(Request $request, string $sip)
|
public function search(Request $request, string $sip)
|
||||||
{
|
{
|
||||||
$account = $request->space->accounts()->sip($sip)->first();
|
$account = $request->space->accounts()->sip($sip)->with('callForwardings')->first();
|
||||||
|
|
||||||
if (!$account)
|
if (!$account)
|
||||||
abort(404, 'SIP address not found');
|
abort(404, 'SIP address not found');
|
||||||
|
|
@ -77,7 +77,7 @@ class AccountController extends Controller
|
||||||
|
|
||||||
public function searchByEmail(Request $request, string $email)
|
public function searchByEmail(Request $request, string $email)
|
||||||
{
|
{
|
||||||
$account = $request->space->accounts()->where('email', $email)->first();
|
$account = $request->space->accounts()->where('email', $email)->with('callForwardings')->first();
|
||||||
|
|
||||||
if (!$account)
|
if (!$account)
|
||||||
abort(404, 'Email address not found');
|
abort(404, 'Email address not found');
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,18 @@ class SpaceController extends Controller
|
||||||
return $space->refresh();
|
return $space->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resolve(Request $request, string $sip)
|
||||||
|
{
|
||||||
|
$account = $request->space->accounts()->sip($sip)->with('callForwardings')->firstOrFail();
|
||||||
|
$arrayAccount = $account->toArray();
|
||||||
|
unset($arrayAccount['space']);
|
||||||
|
|
||||||
|
return json_encode([
|
||||||
|
'type' => 'account',
|
||||||
|
'payload' => $arrayAccount
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function show(string $domain)
|
public function show(string $domain)
|
||||||
{
|
{
|
||||||
return Space::where('domain', $domain)->firstOrFail();
|
return Space::where('domain', $domain)->firstOrFail();
|
||||||
|
|
|
||||||
26
flexiapi/app/Rules/CallForwardingEnable.php
Normal file
26
flexiapi/app/Rules/CallForwardingEnable.php
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use App\Account;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CallForwardingEnable implements ValidationRule
|
||||||
|
{
|
||||||
|
public function __construct(private Request $request, private Account $account)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if ($value == true) {
|
||||||
|
$filter = $this->request->get('type') == 'always' ? ['away', 'busy'] : ['always'];
|
||||||
|
|
||||||
|
if ($this->account->callForwardings()->whereIn('type', $filter)->where('enabled', true)->exists()) {
|
||||||
|
$fail('type: always and type: always/busy cannot be enabled at the same time');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1177
flexiapi/composer.lock
generated
1177
flexiapi/composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?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::create('call_forwardings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->integer('account_id')->unsigned()->nullable();
|
||||||
|
$table->foreign('account_id')->references('id')
|
||||||
|
->on('accounts')->onDelete('cascade');
|
||||||
|
$table->string('type');
|
||||||
|
$table->string('forward_to');
|
||||||
|
$table->string('sip_uri')->nullable();
|
||||||
|
$table->boolean('enabled')->default(false);
|
||||||
|
$table->integer('contact_id')->unsigned()->nullable();
|
||||||
|
$table->foreign('contact_id')->references('id')
|
||||||
|
->on('accounts')->onDelete('cascade');
|
||||||
|
$table->unique(['account_id', 'type']);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('call_forwardings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"Admin": "Administrateur",
|
"Admin": "Administrateur",
|
||||||
"Administration": "Administration",
|
"Administration": "Administration",
|
||||||
"Admins": "Administrateurs",
|
"Admins": "Administrateurs",
|
||||||
|
"All the calls": "Tous les appels",
|
||||||
"All the admins will be super admins": "Tous les administrateurs seront super-administrateurs",
|
"All the admins will be super admins": "Tous les administrateurs seront super-administrateurs",
|
||||||
"Allow a custom CSS theme": "Autoriser un thème CSS personnalisé",
|
"Allow a custom CSS theme": "Autoriser un thème CSS personnalisé",
|
||||||
"Allow client settings to be overwritten by the provisioning ones": "Écraser la configuration client avec celle du déploiement",
|
"Allow client settings to be overwritten by the provisioning ones": "Écraser la configuration client avec celle du déploiement",
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
"Api Keys": "Clefs d'API",
|
"Api Keys": "Clefs d'API",
|
||||||
"App Configuration": "Configuration de l'App",
|
"App Configuration": "Configuration de l'App",
|
||||||
"App settings": "Paramètres d'application",
|
"App settings": "Paramètres d'application",
|
||||||
|
"No anwser": "Pas de réponse",
|
||||||
"Assistant": "Assistant",
|
"Assistant": "Assistant",
|
||||||
"Best regards,":"Cordialement,",
|
"Best regards,":"Cordialement,",
|
||||||
"Blocked": "Bloqué",
|
"Blocked": "Bloqué",
|
||||||
|
|
@ -38,6 +40,7 @@
|
||||||
"By phone": "Par téléphone",
|
"By phone": "Par téléphone",
|
||||||
"By": "Par",
|
"By": "Par",
|
||||||
"Call Recording": "Enregistrement d'appels",
|
"Call Recording": "Enregistrement d'appels",
|
||||||
|
"Call Forwarding": "Redirection d'appels",
|
||||||
"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.",
|
||||||
|
|
@ -63,6 +66,7 @@
|
||||||
"Connection": "Connexion",
|
"Connection": "Connexion",
|
||||||
"Contacts List": "Liste de Contacts",
|
"Contacts List": "Liste de Contacts",
|
||||||
"Contacts Lists": "Listes de Contacts",
|
"Contacts Lists": "Listes de Contacts",
|
||||||
|
"Contact": "Contact",
|
||||||
"Contacts": "Contacts",
|
"Contacts": "Contacts",
|
||||||
"Copyright text": "Texte droits d'auteurs",
|
"Copyright text": "Texte droits d'auteurs",
|
||||||
"Country code": "Code du pays",
|
"Country code": "Code du pays",
|
||||||
|
|
@ -154,11 +158,13 @@
|
||||||
"No account yet?": "Pas encore de compte ?",
|
"No account yet?": "Pas encore de compte ?",
|
||||||
"No email yet": "Pas d'email pour le moment",
|
"No email yet": "Pas d'email pour le moment",
|
||||||
"No limit": "Sans limite",
|
"No limit": "Sans limite",
|
||||||
|
"No answer": "Pas de réponse",
|
||||||
"No phone yet": "Pas de téléphone pour le moment",
|
"No phone yet": "Pas de téléphone pour le moment",
|
||||||
"Number of minutes to expire the key after the last request.": "Nombre de minutes avant l'expiration de la clef après son dernier usage.",
|
"Number of minutes to expire the key after the last request.": "Nombre de minutes avant l'expiration de la clef après son dernier usage.",
|
||||||
"Open the app": "Ouvrir l'application",
|
"Open the app": "Ouvrir l'application",
|
||||||
"Other information": "Autres informations",
|
"Other information": "Autres informations",
|
||||||
"Outbound proxy": "Outbound proxy",
|
"Outbound proxy": "Outbound proxy",
|
||||||
|
"Line occupied": "Ligne occupée",
|
||||||
"Password": "Mot de passe",
|
"Password": "Mot de passe",
|
||||||
"Phone Countries": "Numéros Internationaux",
|
"Phone Countries": "Numéros Internationaux",
|
||||||
"Phone number": "Numéro de téléphone",
|
"Phone number": "Numéro de téléphone",
|
||||||
|
|
@ -203,8 +209,7 @@
|
||||||
"Separated by commas": "Séparé par des virgules",
|
"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 Adress": "Adresse SIP",
|
||||||
"Sip Adress": "Adresse SIP",
|
|
||||||
"SIP Domain": "Domaine SIP",
|
"SIP Domain": "Domaine SIP",
|
||||||
"Space": "Espace",
|
"Space": "Espace",
|
||||||
"Spaces": "Espaces",
|
"Spaces": "Espaces",
|
||||||
|
|
@ -212,6 +217,7 @@
|
||||||
"Subdomain": "Sous-domaine",
|
"Subdomain": "Sous-domaine",
|
||||||
"Super Admin": "Super Admin",
|
"Super Admin": "Super Admin",
|
||||||
"Super Space": "Super Espace",
|
"Super Space": "Super Espace",
|
||||||
|
"Telephony": "Téléphonie",
|
||||||
"Thank you for registering on :space.":"Merci de vous être inscrit sur :space.",
|
"Thank you for registering on :space.":"Merci de vous être inscrit sur :space.",
|
||||||
"Thanks for the validation": "Nous vous remercions pour la validation",
|
"Thanks for the validation": "Nous vous remercions pour la validation",
|
||||||
"The :attribute should not be a phone number": "Le champ :attribute ne peut pas être un numéro de téléphone",
|
"The :attribute should not be a phone number": "Le champ :attribute ne peut pas être un numéro de téléphone",
|
||||||
|
|
|
||||||
13
flexiapi/public/css/form.css
vendored
13
flexiapi/public/css/form.css
vendored
|
|
@ -400,3 +400,16 @@ div.checkbox:hover > input[type="checkbox"] + label {
|
||||||
div.checkbox > input[type="checkbox"]:checked + label {
|
div.checkbox > input[type="checkbox"]:checked + label {
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Telephony subselect */
|
||||||
|
|
||||||
|
div.select[data-value] ~ div.togglable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.select[data-value=voicemail] ~ div.togglable.voicemail,
|
||||||
|
div.select[data-value=contact] ~ div.togglable.contact,
|
||||||
|
div.select[data-value=sip_uri] ~ div.togglable.sip_uri {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
8
flexiapi/public/scripts/utils.js
vendored
8
flexiapi/public/scripts/utils.js
vendored
|
|
@ -135,3 +135,11 @@ function copyValueTo(from, to, append) {
|
||||||
to.value = value;
|
to.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCheckboxValue(name, value) {
|
||||||
|
let checkbox = document.getElementsByName(name)[0];
|
||||||
|
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<h3>
|
||||||
|
{{ __('Call Forwarding') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form id="edit" method="POST" action="{{ route('admin.account.call_forwardings.update', $account->id) }}" accept-charset="UTF-8">
|
||||||
|
@csrf
|
||||||
|
@method('put')
|
||||||
|
@php($callForwardings = $account->callForwardingsDefault)
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h4>{{ __('All the calls') }}</h4>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<input id="always[enabled]" type="checkbox" @if ($callForwardings['always']->enabled) checked @endif name="always[enabled]"
|
||||||
|
onchange="if (this.checked) { setCheckboxValue('away[enabled]', false); setCheckboxValue('busy[enabled]', false); }">
|
||||||
|
<label for="always[enabled]"></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'always'])
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h4>{{ __('No answer') }}</h4>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<input id="away[enabled]" type="checkbox" @if ($callForwardings['away']->enabled) checked @endif name="away[enabled]"
|
||||||
|
onchange="if (this.checked) { setCheckboxValue('always[enabled]', false); }">
|
||||||
|
<label for="away[enabled]"></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'away'])
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h4>{{ __('Line occupied') }}</h4>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<input id="busy[enabled]" type="checkbox" @if ($callForwardings['busy']->enabled) checked @endif name="busy[enabled]"
|
||||||
|
onchange="if (this.checked) { setCheckboxValue('always[enabled]', false); }">
|
||||||
|
<label for="busy[enabled]"></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'busy'])
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="large">
|
||||||
|
<input class="btn small oppose" type="submit" value="{{ __('Update') }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="select" data-value="{{ $callForwardings[$type]->forward_to }}">
|
||||||
|
<select name="{{ $type }}[forward_to]" onchange="this.parentNode.dataset.value = this.value">
|
||||||
|
<option @if ($callForwardings[$type]->forward_to == 'voicemail') selected @endif value="voicemail">{{ __('Voicemails') }}</option>
|
||||||
|
<option @if ($callForwardings[$type]->forward_to == null || $callForwardings[$type]->forward_to == 'sip_uri') selected @endif value="sip_uri">{{ __('SIP Adress') }}</option>
|
||||||
|
<option @if ($callForwardings[$type]->forward_to == 'contact') selected @endif value="contact">{{ __('Contact') }}</option>
|
||||||
|
</select>
|
||||||
|
<label for="{{ $type }}[forward_to]">{{ __('Destination') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="togglable sip_uri">
|
||||||
|
<input placeholder="sip:username@server.com" list="contacts" name="{{ $type }}[sip_uri]" type="text" id="busy[sip_uri]" value="{{ $callForwardings[$type]->sip_uri }}">
|
||||||
|
<label for="sip">{{ __('SIP Adress') }}</label>
|
||||||
|
@include('parts.errors', ['name' => $type . '.sip_uri'])
|
||||||
|
</div>
|
||||||
|
<div class="togglable voicemail"></div>
|
||||||
|
<div class="select togglable contact">
|
||||||
|
<select name="{{ $type }}[contact_id]">
|
||||||
|
@foreach (resolveUserContacts($account)->get() as $contact)
|
||||||
|
<option @if ($callForwardings[$type]->contact_id == $contact->id) selected @endif value="{{ $contact->id }}">{{ $contact->identifier }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<label for="contact">{{ __('Contact') }}</label>
|
||||||
|
@include('parts.errors', ['name' => $type . '.contact'])
|
||||||
|
</div>
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
@foreach ($account->contactsLists as $contactsList)
|
@foreach ($account->contactsLists as $contactsList)
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('admin.spaces.contacts_lists.edit', [$space, $contactsList->id]) }}">{{ $contactsList->title }}</a>
|
<a href="{{ route('admin.spaces.contacts_lists.edit', [$account->space, $contactsList->id]) }}">{{ $contactsList->title }}</a>
|
||||||
<small>{{ $contactsList->contacts_count }} {{ __('Contacts') }}</small>
|
<small>{{ $contactsList->contacts_count }} {{ __('Contacts') }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
@include('parts.tabs', [
|
@include('parts.tabs', [
|
||||||
'items' => [
|
'items' => [
|
||||||
route('admin.account.show', $account) => __('Information'),
|
route('admin.account.show', $account) => __('Information'),
|
||||||
|
route('admin.account.telephony.show', $account) => __('Telephony'),
|
||||||
route('admin.account.contact.index', $account) => __('Contacts'),
|
route('admin.account.contact.index', $account) => __('Contacts'),
|
||||||
route('admin.account.statistics.show_call_logs', $account) => __('Calls logs'),
|
route('admin.account.statistics.show_call_logs', $account) => __('Calls logs'),
|
||||||
route('admin.account.statistics.show', $account) => __('Statistics'),
|
route('admin.account.statistics.show', $account) => __('Statistics'),
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="card">
|
<div class="card large">
|
||||||
<h3>
|
<h3>
|
||||||
{{ __('Devices') }}
|
{{ __('Devices') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -211,53 +211,6 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>
|
|
||||||
{{ __('Voicemails') }}
|
|
||||||
</h3>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ __('Created') }}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@if ($account->uploadedVoicemails->isEmpty())
|
|
||||||
<tr class="empty">
|
|
||||||
<td colspan="2">{{ __('Empty') }}</td>
|
|
||||||
</tr>
|
|
||||||
@endif
|
|
||||||
@foreach ($account->uploadedVoicemails as $voicemail)
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{ $voicemail->created_at }}
|
|
||||||
@if ($voicemail->url)
|
|
||||||
<a style="margin-left: 1rem;" href="{{ $voicemail->download_url }}" download>
|
|
||||||
<i class="ph ph-download"></i>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
@if ($voicemail->sip_from)
|
|
||||||
<br/>
|
|
||||||
<small>{{ $voicemail->sip_from }}</small>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@if ($voicemail->url)
|
|
||||||
<audio class="oppose" controls src="{{ $voicemail->url }}"></audio>
|
|
||||||
<a type="button"
|
|
||||||
class="oppose btn tertiary"
|
|
||||||
href="{{ route('admin.account.file.delete', [$account, $voicemail->id]) }}">
|
|
||||||
<i class="ph ph-trash"></i>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card large">
|
<div class="card large">
|
||||||
<a class="btn small oppose" href="{{ route('admin.account.dictionary.create', $account) }}">
|
<a class="btn small oppose" href="{{ route('admin.account.dictionary.create', $account) }}">
|
||||||
<i class="ph ph-plus"></i>
|
<i class="ph ph-plus"></i>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
@extends('layouts.main')
|
||||||
|
|
||||||
|
@section('breadcrumb')
|
||||||
|
@include('admin.parts.breadcrumb.accounts.show', ['account' => $account])
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<header>
|
||||||
|
<h1><i class="ph ph-phone"></i> {{ $account->identifier }}</h1>
|
||||||
|
</header>
|
||||||
|
@include('admin.account.parts.tabs')
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>
|
||||||
|
{{ __('Voicemails') }}
|
||||||
|
</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Created') }}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if ($account->uploadedVoicemails->isEmpty())
|
||||||
|
<tr class="empty">
|
||||||
|
<td colspan="2">{{ __('Empty') }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@foreach ($account->uploadedVoicemails as $voicemail)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ $voicemail->created_at }}
|
||||||
|
@if ($voicemail->url)
|
||||||
|
<a style="margin-left: 1rem;" href="{{ $voicemail->download_url }}" download>
|
||||||
|
<i class="ph ph-download"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@if ($voicemail->sip_from)
|
||||||
|
<br/>
|
||||||
|
<small>{{ $voicemail->sip_from }}</small>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if ($voicemail->url)
|
||||||
|
<audio class="oppose" controls src="{{ $voicemail->url }}"></audio>
|
||||||
|
<a type="button"
|
||||||
|
class="oppose btn tertiary"
|
||||||
|
href="{{ route('admin.account.file.delete', [$account, $voicemail->id]) }}">
|
||||||
|
<i class="ph ph-trash"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
@include('admin.account.call_forwardings.edit', ['account' => $account])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
## Account Call Forwardings
|
||||||
|
|
||||||
|
### `GET /accounts/{id/me}/call_forwardings`
|
||||||
|
<span class="badge badge-info">User</span>
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Return the user Call Forwardings.
|
||||||
|
|
||||||
|
### `GET /accounts/{id/me}/call_forwardings/{call_forwarding_id}`
|
||||||
|
<span class="badge badge-info">User</span>
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Return a Call Forwarding configuration.
|
||||||
|
|
||||||
|
### `POST /accounts/{id/me}/call_forwardings`
|
||||||
|
<span class="badge badge-info">User</span>
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Create a new Call Forwarding configuration.
|
||||||
|
|
||||||
|
JSON parameters:
|
||||||
|
|
||||||
|
* `type` **required**, must be `always`, `away` or `busy`, one of each declaration maximum per account
|
||||||
|
* `forward_to` **required**, must be `sip_uri`, `voicemail` or `contact`
|
||||||
|
* `sip_uri` **required if `forward_to` is set to `sip_uri`**, must be a SIP URI, must be set when `forward_to` set to `sip_uri`
|
||||||
|
* `contact_id` **required if `forward_to` is set to `contact`**, must be a valid `contact_id` of the contact
|
||||||
|
* `enabled` **required**, boolean. If `type: always` is enabled `away` and `busy` must be disabled. If `type: away or busy` are enabled `always` must be disabled.
|
||||||
|
|
||||||
|
### `PUT /accounts/{id/me}/call_forwardings/{call_forwarding_id}`
|
||||||
|
<span class="badge badge-info">User</span>
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Create a new Call Forwarding configuration.
|
||||||
|
|
||||||
|
JSON parameters:
|
||||||
|
|
||||||
|
* `forward_to` **required**, must be `sip_uri`, `voicemail` or `contact`
|
||||||
|
* `sip_uri` **required if `forward_to` is set to `sip_uri`**, must be a SIP URI, must be set when `forward_to` set to `sip_uri`
|
||||||
|
* `contact_id` **required if `forward_to` is set to `contact`**, must be a valid `contact_id` of the contact
|
||||||
|
* `enabled` **required**, boolean. If `type: always` is enabled `away` and `busy` must be disabled. If `type: away or busy` are enabled `always` must be disabled.
|
||||||
|
|
||||||
|
### `DELETE /accounts/{id/me}/call_forwardings/{call_forwarding_id}`
|
||||||
|
<span class="badge badge-info">User</span>
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Remove a Call Forwarding configuration.
|
||||||
|
|
@ -9,6 +9,19 @@
|
||||||
|
|
||||||
Returns `pong`
|
Returns `pong`
|
||||||
|
|
||||||
|
## SIP URI Resolving
|
||||||
|
|
||||||
|
### `GET /resolve/{sip}`
|
||||||
|
<span class="badge badge-warning">Admin</span>
|
||||||
|
|
||||||
|
Resolve a specific `SIP URI` in the space.
|
||||||
|
All the resolved object can be reached using direct API endpoints. This endpoint is only there as a shortcut and should be used sparingly.
|
||||||
|
|
||||||
|
Will return a JSON message with:
|
||||||
|
|
||||||
|
* `type` resolved API object `type`, can be `acccount`
|
||||||
|
* `payload` that contains the resolved object
|
||||||
|
|
||||||
@include('api.documentation.spaces')
|
@include('api.documentation.spaces')
|
||||||
|
|
||||||
@include('api.documentation.spaces.carddav')
|
@include('api.documentation.spaces.carddav')
|
||||||
|
|
@ -25,6 +38,8 @@ Returns `pong`
|
||||||
|
|
||||||
@include('api.documentation.accounts.contacts_lists')
|
@include('api.documentation.accounts.contacts_lists')
|
||||||
|
|
||||||
|
@include('api.documentation.accounts.call_forwarding')
|
||||||
|
|
||||||
@include('api.documentation.accounts.contacts')
|
@include('api.documentation.accounts.contacts')
|
||||||
|
|
||||||
@include('api.documentation.accounts.dictionary')
|
@include('api.documentation.accounts.dictionary')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
@extends('errors::minimal')
|
@extends('errors::minimal')
|
||||||
|
|
||||||
@section('title', __('Service Unavailable'))
|
|
||||||
@section('code', '503')
|
@section('code', '503')
|
||||||
@section('message', $exception->getMessage())
|
|
||||||
|
@if (app()->isDownForMaintenance())
|
||||||
|
@section('title', __('We will be back soon!'))
|
||||||
|
@section('message', 'Sorry for the inconvenience but we are performing some maintenance at the moment.')
|
||||||
|
@else
|
||||||
|
@section('title', __('Service Unavailable'))
|
||||||
|
@section('message', $exception->getMessage())
|
||||||
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
@if (isset($errors) && isset($name) && count($errors->get($name)) > 0)
|
@if (isset($errors) && isset($name) && count($errors->get($name)) > 0)
|
||||||
@foreach ($errors->get($name) as $error)
|
@foreach ($errors->get($name) as $error)
|
||||||
<small class="error">
|
<small class="error">
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
use App\Http\Controllers\Api\Account\AccountController;
|
use App\Http\Controllers\Api\Account\AccountController;
|
||||||
use App\Http\Controllers\Api\Account\ApiKeyController;
|
use App\Http\Controllers\Api\Account\ApiKeyController;
|
||||||
use App\Http\Controllers\Api\Account\AuthTokenController;
|
use App\Http\Controllers\Api\Account\AuthTokenController;
|
||||||
|
use App\Http\Controllers\Api\Account\CallForwardingController;
|
||||||
use App\Http\Controllers\Api\Account\ContactController;
|
use App\Http\Controllers\Api\Account\ContactController;
|
||||||
use App\Http\Controllers\Api\Account\CreationRequestToken;
|
use App\Http\Controllers\Api\Account\CreationRequestToken;
|
||||||
use App\Http\Controllers\Api\Account\CreationTokenController;
|
use App\Http\Controllers\Api\Account\CreationTokenController;
|
||||||
|
|
@ -33,6 +34,7 @@ use App\Http\Controllers\Api\Account\RecoveryTokenController;
|
||||||
use App\Http\Controllers\Api\Account\VcardsStorageController;
|
use App\Http\Controllers\Api\Account\VcardsStorageController;
|
||||||
use App\Http\Controllers\Api\Account\VoicemailController;
|
use App\Http\Controllers\Api\Account\VoicemailController;
|
||||||
use App\Http\Controllers\Api\Admin\Account\ActionController;
|
use App\Http\Controllers\Api\Admin\Account\ActionController;
|
||||||
|
use App\Http\Controllers\Api\Admin\Account\CallForwardingController as AdminCallForwardingController;
|
||||||
use App\Http\Controllers\Api\Admin\Account\CardDavCredentialsController;
|
use App\Http\Controllers\Api\Admin\Account\CardDavCredentialsController;
|
||||||
use App\Http\Controllers\Api\Admin\Account\ContactController as AdminContactController;
|
use App\Http\Controllers\Api\Admin\Account\ContactController as AdminContactController;
|
||||||
use App\Http\Controllers\Api\Admin\Account\CreationTokenController as AdminCreationTokenController;
|
use App\Http\Controllers\Api\Admin\Account\CreationTokenController as AdminCreationTokenController;
|
||||||
|
|
@ -111,6 +113,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
|
||||||
|
|
||||||
Route::apiResource('vcards-storage', VcardsStorageController::class);
|
Route::apiResource('vcards-storage', VcardsStorageController::class);
|
||||||
Route::apiResource('voicemails', VoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]);
|
Route::apiResource('voicemails', VoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]);
|
||||||
|
Route::apiResource('call_forwardings', CallForwardingController::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::group(['middleware' => ['auth.admin']], function () {
|
Route::group(['middleware' => ['auth.admin']], function () {
|
||||||
|
|
@ -134,6 +137,8 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
|
||||||
Route::post('phone_countries/{code}/deactivate', [AdminPhoneCountryController::class, 'deactivate']);
|
Route::post('phone_countries/{code}/deactivate', [AdminPhoneCountryController::class, 'deactivate']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('resolve/{sip}', [SpaceController::class, 'resolve']);
|
||||||
|
|
||||||
// Account creation token
|
// Account creation token
|
||||||
Route::post('account_creation_tokens', [AdminCreationTokenController::class, 'create']);
|
Route::post('account_creation_tokens', [AdminCreationTokenController::class, 'create']);
|
||||||
|
|
||||||
|
|
@ -181,6 +186,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
|
||||||
Route::apiResource('account_types', TypeController::class);
|
Route::apiResource('account_types', TypeController::class);
|
||||||
Route::apiResource('accounts/{id}/vcards-storage', AdminVcardsStorageController::class);
|
Route::apiResource('accounts/{id}/vcards-storage', AdminVcardsStorageController::class);
|
||||||
Route::apiResource('accounts/{id}/voicemails', AdminVoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]);
|
Route::apiResource('accounts/{id}/voicemails', AdminVoicemailController::class, ['only' => ['index', 'show', 'store', 'destroy']]);
|
||||||
|
Route::apiResource('accounts/{id}/call_forwardings', AdminCallForwardingController::class);
|
||||||
|
|
||||||
Route::apiResource('contacts_lists', ContactsListController::class);
|
Route::apiResource('contacts_lists', ContactsListController::class);
|
||||||
Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () {
|
Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ use App\Http\Controllers\Account\VcardsStorageController;
|
||||||
use App\Http\Controllers\Admin\Account\AccountTypeController;
|
use App\Http\Controllers\Admin\Account\AccountTypeController;
|
||||||
use App\Http\Controllers\Admin\Account\ActionController;
|
use App\Http\Controllers\Admin\Account\ActionController;
|
||||||
use App\Http\Controllers\Admin\Account\ActivityController;
|
use App\Http\Controllers\Admin\Account\ActivityController;
|
||||||
|
use App\Http\Controllers\Admin\Account\CallForwardingController;
|
||||||
use App\Http\Controllers\Admin\Account\CardDavCredentialsController;
|
use App\Http\Controllers\Admin\Account\CardDavCredentialsController;
|
||||||
use App\Http\Controllers\Admin\Account\ContactController;
|
use App\Http\Controllers\Admin\Account\ContactController;
|
||||||
use App\Http\Controllers\Admin\Account\DeviceController as AdminAccountDeviceController;
|
use App\Http\Controllers\Admin\Account\DeviceController as AdminAccountDeviceController;
|
||||||
|
|
@ -43,6 +44,7 @@ use App\Http\Controllers\Admin\Account\DictionaryController;
|
||||||
use App\Http\Controllers\Admin\Account\FileController as AdminFileController;
|
use App\Http\Controllers\Admin\Account\FileController as AdminFileController;
|
||||||
use App\Http\Controllers\Admin\Account\ImportController;
|
use App\Http\Controllers\Admin\Account\ImportController;
|
||||||
use App\Http\Controllers\Admin\Account\StatisticsController as AdminAccountStatisticsController;
|
use App\Http\Controllers\Admin\Account\StatisticsController as AdminAccountStatisticsController;
|
||||||
|
use App\Http\Controllers\Admin\Account\TelephonyController;
|
||||||
use App\Http\Controllers\Admin\Account\TypeController;
|
use App\Http\Controllers\Admin\Account\TypeController;
|
||||||
use App\Http\Controllers\Admin\AccountController as AdminAccountController;
|
use App\Http\Controllers\Admin\AccountController as AdminAccountController;
|
||||||
use App\Http\Controllers\Admin\ApiKeyController as AdminApiKeyController;
|
use App\Http\Controllers\Admin\ApiKeyController as AdminApiKeyController;
|
||||||
|
|
@ -312,6 +314,14 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () {
|
||||||
Route::delete('/', 'destroy')->name('destroy');
|
Route::delete('/', 'destroy')->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::name('telephony.')->prefix('{account}/telephony')->controller(TelephonyController::class)->group(function () {
|
||||||
|
Route::get('/', 'show')->name('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::name('call_forwardings.')->prefix('{account}/call_forwardings')->controller(CallForwardingController::class)->group(function () {
|
||||||
|
Route::put('/', 'update')->name('update');
|
||||||
|
});
|
||||||
|
|
||||||
Route::name('device.')->prefix('{account}/devices')->controller(AdminAccountDeviceController::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');
|
||||||
|
|
|
||||||
162
flexiapi/tests/Feature/ApiAccountCallForwardingTest.php
Normal file
162
flexiapi/tests/Feature/ApiAccountCallForwardingTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
Flexisip Account Manager is a set of tools to manage SIP accounts.
|
||||||
|
Copyright (C) 2026 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 Tests\Feature;
|
||||||
|
|
||||||
|
use App\Account;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use function PHPUnit\Framework\assertJson;
|
||||||
|
|
||||||
|
class ApiAccountCallForwardingTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $route = '/api/accounts/me/call_forwardings';
|
||||||
|
protected $method = 'POST';
|
||||||
|
|
||||||
|
public function testResolving()
|
||||||
|
{
|
||||||
|
$account = Account::factory()->create();
|
||||||
|
$account->generateUserApiKey();
|
||||||
|
|
||||||
|
$uri = 'sip:uri';
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => $uri,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertStatus(201);
|
||||||
|
|
||||||
|
$admin = Account::factory()->admin()->create();
|
||||||
|
$admin->generateUserApiKey();
|
||||||
|
|
||||||
|
$this->keyAuthenticated($admin)
|
||||||
|
->get('/api/resolve/' . $account->identifier)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertJsonFragment(['type' => 'account']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCrud()
|
||||||
|
{
|
||||||
|
$account = Account::factory()->create();
|
||||||
|
$account->generateUserApiKey();
|
||||||
|
$admin = Account::factory()->admin()->create();
|
||||||
|
$admin->generateUserApiKey();
|
||||||
|
|
||||||
|
$uri = 'sip:uri';
|
||||||
|
|
||||||
|
// Contacts
|
||||||
|
|
||||||
|
$contactAccount = Account::factory()->create();
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'contact',
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['contact_id']);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'contact',
|
||||||
|
'contact_id' => $contactAccount->id,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['contact_id']);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($admin)
|
||||||
|
->json($this->method, '/api/accounts/' . $account->id . '/contacts/' . $contactAccount->id)
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$response = $this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'contact',
|
||||||
|
'contact_id' => $contactAccount->id,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertStatus(201);
|
||||||
|
|
||||||
|
// SIP URI
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => null,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['type']);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json('DELETE', $this->route . '/' . $response['id'])
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => null,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['sip_uri']);
|
||||||
|
|
||||||
|
$response = $this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => $uri,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertStatus(201);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'away',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => $uri,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertJsonValidationErrors(['enabled']);
|
||||||
|
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json('PUT', $this->route . '/' . $response->json()['id'], [
|
||||||
|
'type' => 'always',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => $uri,
|
||||||
|
'enabled' => false
|
||||||
|
])
|
||||||
|
->assertStatus(200);
|
||||||
|
|
||||||
|
$this->keyAuthenticated($account)
|
||||||
|
->json($this->method, $this->route, [
|
||||||
|
'type' => 'away',
|
||||||
|
'forward_to' => 'sip_uri',
|
||||||
|
'sip_uri' => $uri,
|
||||||
|
'enabled' => true
|
||||||
|
])
|
||||||
|
->assertStatus(201);
|
||||||
|
|
||||||
|
$this->assertCount(2, $this->keyAuthenticated($account)
|
||||||
|
->json('GET', $this->route)->json());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue