Fix FLEXIAPI-420 Call forwarding user panel

Fix FLEXIAPI-444 Complete documentation regarding account creation with a domain
This commit is contained in:
Timothée Jaussoin 2026-02-26 17:19:46 +01:00
parent 450844ba33
commit 57381f1454
22 changed files with 179 additions and 91 deletions

View file

@ -40,6 +40,13 @@ class AccountController extends Controller
]);
}
public function telephony(Request $request)
{
return view('account.telephony', [
'account' => $request->user()
]);
}
public function store(WebRequest $request)
{
$account = (new AccountService())->store($request);

View file

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use \App\Http\Controllers\Admin\Account\CallForwardingController as AdminCallForwardingController;
class CallForwardingController extends Controller
{
public function update(Request $request)
{
return (new AdminCallForwardingController)->update($request, $request->user()->id);
}
}

View file

@ -5,6 +5,8 @@ namespace App\Http\Controllers\Account;
use App\AccountFile;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;
use \App\Http\Controllers\Admin\Account\FileController as AdminFileController;
class FileController extends Controller
{
@ -29,4 +31,14 @@ class FileController extends Controller
return Storage::download($file->path);
}
public function delete(Request $request, string $fileId)
{
return (new AdminFileController)->delete($request->user()->id, $fileId);
}
public function destroy(Request $request, string $fileId)
{
return (new AdminFileController)->destroy($request, $request->user()->id, $fileId);
}
}

View file

@ -21,7 +21,6 @@ namespace App\Http\Controllers\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class PasswordController extends Controller

View file

@ -49,7 +49,7 @@ class ProvisioningController extends Controller
public function qrcode(Request $request, string $provisioningToken)
{
$account = Account::withoutGlobalScopes()
Account::withoutGlobalScopes()
->where('id', function ($query) use ($provisioningToken) {
$query->select('account_id')
->from('provisioning_tokens')

View file

@ -19,15 +19,21 @@ class CallForwardingController extends Controller
$request->validate([
'always.forward_to' => $forwardTo,
'always.sip_uri' => 'nullable|starts_with:sip:|required_if:always.forward_to,sip_uri',
'always.sip_uri' => array_key_exists('enabled', $request->get('always'))
? 'nullable|starts_with:sip:|required_if:always.forward_to,sip_uri'
: 'nullable',
'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.sip_uri' => array_key_exists('enabled', $request->get('away'))
? 'nullable|starts_with:sip:|required_if:away.forward_to,sip_uri'
: 'nullable',
'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.sip_uri' => array_key_exists('enabled', $request->get('busy'))
? 'nullable|starts_with:sip:|required_if:busy.forward_to,sip_uri'
: 'nullable',
'busy.contact_id' => ['required_if:busy.forward_to,contact', Rule::in($contactsIds)],
]);
@ -72,6 +78,8 @@ class CallForwardingController extends Controller
$busyForwarding->save();
}
return redirect()->route('admin.account.telephony.show', $account);
return $request->user()
? redirect()->route('admin.account.telephony.show', $account)
: redirect()->route('account.telephony');
}
}

View file

@ -13,7 +13,7 @@ class FileController extends Controller
$account = Account::findOrFail($accountId);
$file = $account->files()->where('id', $fileId)->firstOrFail();
return view('admin.account.file.delete', [
return view('account.file.delete', [
'account' => $account,
'file' => $file
]);
@ -27,6 +27,8 @@ class FileController extends Controller
->firstOrFail();
$accountFile->delete();
return redirect()->route('admin.account.show', $account)->withFragment('#files');
return $request->user()->admin
? redirect()->route('admin.account.telephony.show', $account)->withFragment('#files')
: redirect()->route('account.telephony')->withFragment('#files');
}
}

View file

@ -21,6 +21,7 @@
"Admin": "Administrateur",
"Administration": "Administration",
"Admins": "Administrateurs",
"All incoming calls are forwarded, whether you answer, decline the call or are already on a call.": "Tous les appels entrants sont renvoyés, que vous répondiez, refusiez lappel ou soyez déjà en ligne.",
"All the calls": "Tous les appels",
"All the admins will be super admins": "Tous les administrateurs seront super-administrateurs",
"Allow a custom CSS theme": "Autoriser un thème CSS personnalisé",
@ -39,6 +40,8 @@
"By email": "Inscription par email",
"By phone": "Par téléphone",
"By": "Par",
"Calls are only forwarded when your line is busy with another call.": "Les appels sont renvoyés uniquement lorsque votre ligne est occupée par un autre appel.",
"Calls are only forwarded if you do not answer or if you decline the call.": "Les appels sont renvoyés uniquement si vous ne répondez pas ou si vous refusez lappel.",
"Call Recording": "Enregistrement d'appels",
"Call Forwarding": "Redirection d'appels",
"Calls logs": "Journaux d'appel",

View file

@ -2,7 +2,7 @@
{{ __('Call Forwarding') }}
</h3>
<form id="edit" method="POST" action="{{ route('admin.account.call_forwardings.update', $account->id) }}" accept-charset="UTF-8">
<form id="edit" method="POST" action="@if ($account->admin) {{ route('admin.account.call_forwardings.update', $account->id) }}@else{{ route('account.call_forwardings.update') }}@endif" accept-charset="UTF-8">
@csrf
@method('put')
@php($callForwardings = $account->callForwardingsDefault)
@ -16,7 +16,10 @@
<label for="always[enabled]"></label>
</div>
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'always'])
@include('account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'always'])
@if (!$account->admin)
<small class="large">{{ __('All incoming calls are forwarded, whether you answer, decline the call or are already on a call.') }}</small>
@endif
</section>
<section>
@ -28,7 +31,10 @@
<label for="away[enabled]"></label>
</div>
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'away'])
@include('account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'away'])
@if (!$account->admin)
<small class="large">{{ __('Calls are only forwarded when your line is busy with another call.') }}</small>
@endif
</section>
<section>
@ -40,7 +46,10 @@
<label for="busy[enabled]"></label>
</div>
@include('admin.account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'busy'])
@include('account.call_forwardings.edit_select_part', ['callForwarding' => $callForwardings, 'type' => 'busy'])
@if (!$account->admin)
<small class="large">{{ __('Calls are only forwarded if you do not answer or if you decline the call.') }}</small>
@endif
</section>
<div class="large">

View file

@ -1,4 +1,4 @@
<div class="select" data-value="{{ $callForwardings[$type]->forward_to }}">
<div class="select" data-value="{{ $callForwardings[$type]->forward_to ?? 'sip_uri' }}">
<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>

View file

@ -0,0 +1,17 @@
@extends('layouts.main')
@section('content')
<header>
<h1><i class="ph ph-phone"></i> {{ __('Telephony') }}</h1>
</header>
<div class="grid">
<div class="card">
@include('account.voicemails.index', ['account' => $account, 'admin' => false])
</div>
<div class="card">
@include('account.call_forwardings.edit', ['account' => $account])
</div>
</div>
@endsection

View file

@ -0,0 +1,49 @@
<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"
@if ($admin)
href="{{ route('admin.account.file.delete', [$account, $voicemail->id]) }}"
@else
href="{{ route('account.file.delete', [$voicemail->id]) }}"
@endif
>
<i class="ph ph-trash"></i>
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>

View file

@ -12,54 +12,7 @@
<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])
@include('account.call_forwardings.edit', ['account' => $account])
</div>
</div>
@endsection

View file

@ -83,7 +83,7 @@ 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.
* `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. Return 403 if the value doesn't resolve to a Space or if the Space is full.
* `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`

View file

@ -1,13 +1,11 @@
## Voicemails
### `GET /accounts/{id/me}/voicemails`
<span class="badge badge-warning">Admin</span>
### `GET /accounts/me/voicemails`
<span class="badge badge-info">User</span>
Return the currently stored voicemails
### `GET /accounts/{id/me}/voicemails/{uuid}`
<span class="badge badge-warning">Admin</span>
### `GET /accounts/me/voicemails/{uuid}`
<span class="badge badge-info">User</span>
```

View file

@ -1,7 +1,14 @@
@if (isset($errors) && $errors->isNotEmpty())
@if (isset($errors) && isset($name) && count($errors->get($name)) > 0)
@foreach ($errors->get($name) as $error)
<small class="error">
{{ $error }}
</small>
@endforeach
@elseif (isset($errors) && $errors->isNotEmpty() && is_int($errors->keys()[0]) && !isset($name))
<ul class="errors">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif

View file

@ -15,10 +15,16 @@
}
@endphp
@foreach ($items as $route => $value)
<a @if (str_starts_with(url()->current(), route($route)))class="current"@endif href="{{ route($route) }}">
<i class="ph ph-{{ $value['icon'] }}"></i>
{{ $value['title'] }}
</a>
@endforeach
@include('parts.sidebar_items', ['items' => $items])
<hr />
@php
$items = [];
$items['account.dashboard'] = ['title' => __('My Account'), 'icon' => 'gauge'];
$items['account.telephony'] = ['title' => __('Telephony'), 'icon' => 'phone'];
@endphp
@include('parts.sidebar_items', ['items' => $items])
</nav>

View file

@ -0,0 +1,6 @@
@foreach ($items as $route => $value)
<a @if (str_starts_with(url()->current(), route($route)))class="current"@endif href="{{ route($route) }}">
<i class="ph ph-{{ $value['icon'] }}"></i>
{{ $value['title'] }}
</a>
@endforeach

View file

@ -185,7 +185,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
Route::apiResource('accounts/{id}/actions', ActionController::class);
Route::apiResource('account_types', TypeController::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' => ['store', 'destroy']]);
Route::apiResource('accounts/{id}/call_forwardings', AdminCallForwardingController::class);
Route::apiResource('contacts_lists', ContactsListController::class);

View file

@ -22,6 +22,7 @@ use App\Http\Controllers\Account\AccountController;
use App\Http\Controllers\Account\ApiKeyController;
use App\Http\Controllers\Account\AuthenticateController;
use App\Http\Controllers\Account\AuthTokenController;
use App\Http\Controllers\Account\CallForwardingController;
use App\Http\Controllers\Account\ContactVcardController;
use App\Http\Controllers\Account\CreationRequestTokenController;
use App\Http\Controllers\Account\DeviceController;
@ -36,7 +37,7 @@ use App\Http\Controllers\Account\VcardsStorageController;
use App\Http\Controllers\Admin\Account\AccountTypeController;
use App\Http\Controllers\Admin\Account\ActionController;
use App\Http\Controllers\Admin\Account\ActivityController;
use App\Http\Controllers\Admin\Account\CallForwardingController;
use App\Http\Controllers\Admin\Account\CallForwardingController as AdminCallForwardingController;
use App\Http\Controllers\Admin\Account\CardDavCredentialsController;
use App\Http\Controllers\Admin\Account\ContactController;
use App\Http\Controllers\Admin\Account\DeviceController as AdminAccountDeviceController;
@ -80,7 +81,7 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () {
});
});
Route::name('file.')->prefix('files')->controller(FileController::class)->group(function () {
Route::name('file.')->prefix('f')->controller(FileController::class)->group(function () {
Route::get('{uuid}/{name}', 'show')->name('show');
Route::get('{uuid}/{name}/download', 'download')->name('download');
});
@ -135,6 +136,11 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () {
Route::post('/', 'store')->name('email.update');
});
Route::name('file.')->prefix('files')->controller(FileController::class)->group(function () {
Route::get('{uuid}/delete', 'delete')->name('delete');
Route::delete('{uuid}', 'destroy')->name('destroy');
});
Route::middleware(['feature.phone_registration'])->group(function () {
Route::prefix('phone')->controller(PhoneController::class)->group(function () {
Route::get('change', 'change')->name('phone.change');
@ -150,8 +156,13 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () {
Route::delete('/', 'destroy')->name('destroy');
});
Route::name('call_forwardings.')->prefix('call_forwardings')->controller(CallForwardingController::class)->group(function () {
Route::put('/', 'update')->name('update');
});
Route::controller(AccountController::class)->group(function () {
Route::get('dashboard', 'dashboard')->name('dashboard');
Route::get('telephony', 'telephony')->name('telephony');
Route::get('delete', 'delete')->name('delete');
Route::delete('delete', 'destroy')->name('destroy');
@ -318,7 +329,7 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () {
Route::get('/', 'show')->name('show');
});
Route::name('call_forwardings.')->prefix('{account}/call_forwardings')->controller(CallForwardingController::class)->group(function () {
Route::name('call_forwardings.')->prefix('{account}/call_forwardings')->controller(AdminCallForwardingController::class)->group(function () {
Route::put('/', 'update')->name('update');
});

View file

@ -105,23 +105,9 @@ class ApiVoicemailTest extends TestCase
$uuid = $accountFile->json()['id'];
$this->keyAuthenticated($admin)
->get($adminRoute . '/' . $uuid)
->assertJsonFragment(['id' => $uuid]);
$this->keyAuthenticated($admin)
->get($adminRoute . '/' . $uuid)
->assertJsonFragment([
'id' => $uuid
]);
$this->keyAuthenticated($admin)
->delete($adminRoute . '/' . $uuid)
->assertOk();
$this->keyAuthenticated($admin)
->get($adminRoute . '/' . $uuid)
->assertNotFound();
}
public function testUpload()