mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-01-17 01:58:07 +00:00
Fix FLEXIAPI-233 Add External Accounts (new version)
This commit is contained in:
parent
a8e81908ee
commit
7cb63f3e51
32 changed files with 751 additions and 108 deletions
|
|
@ -26,6 +26,7 @@ v1.7
|
|||
- Fix FLEXIAPI-261 Remove the TURN part in the XML provisioning (and only keep the API endpoint)
|
||||
- Fix FLEXIAPI-275 Add names in Spaces
|
||||
- Fix FLEXIAPI-278 Complete and reorganize the Markdown documentation
|
||||
- Fix FLEXIAPI-233 Add External Accounts (new version)
|
||||
|
||||
v1.6
|
||||
----
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ This is the host that you'll define in the Apache or webserver VirtualHost:
|
|||
ServerName flexiapi-domain.tld
|
||||
ServerAlias *.flexiapi-domain.tld
|
||||
|
||||
If you are planning to manage several SIP domains (see Spaces bellow) a wildcard `ServerAlias` as above is required.
|
||||
If you are planning to manage several Spaces (see Spaces bellow) a wildcard `ServerAlias` as above is required.
|
||||
|
||||
## 3.2. Database migration
|
||||
|
||||
|
|
@ -56,14 +56,14 @@ Then configure the database connection parameters and migrate the tables. The fi
|
|||
|
||||
Since the 1.6 FlexiAPI can manage different SIP Domains on separate HTTP subdomains.
|
||||
|
||||
A Space is defined as a specific HTTP subdomain of `APP_ROOT_HOST` and is linked to a specific SIP Domain. It is also possible to host one specific Space directly under `APP_ROOT_HOST`.
|
||||
A Space is defined as a specific HTTP subdomain of `APP_ROOT_HOST` and is linked to a specific SIP Domain. It is also possible to host one (and only one) specific Space directly under `APP_ROOT_HOST`.
|
||||
|
||||
By default administrator accounts in Spaces will only see the accounts of their own Space (that have the same SIP Domain).
|
||||
However it is possible to define a Space as a "SuperSpace" allowing the admins to see all the other Spaces and accounts and create/edit/delete the other Spaces.
|
||||
|
||||
## 4.1. Setup the first Space
|
||||
|
||||
You will need to create the first Space manually, generally as a SuperSpace, after that the other Spaces can directly be created in your browser through the web panel.
|
||||
You will need to create the first Space manually, generally as a SuperSpace, after that the other Spaces can directly be created in your browser through the Web Panels.
|
||||
|
||||
php artisan spaces:create-update {sip_domain} {host} {name} {--super}
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ Create a first administator account:
|
|||
|
||||
For example:
|
||||
|
||||
php artisan accounts:create-admin-account -u admin -p strong_password -d my-company-sip-domain.tld
|
||||
php artisan accounts:create-admin-account -u admin -p strong_password -d company-sip-domain.tld
|
||||
|
||||
You can now try to authenticate on the web panel and continue the setup using your admin account.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ php artisan spaces:create-update beta.sip beta.myhost.com "Beta Space"
|
|||
...
|
||||
```
|
||||
|
||||
4. Configure your web server to point the `APP_ROOT_HOST` and subdomains to the app.
|
||||
4. Configure your web server to point the `APP_ROOT_HOST` and subdomains to the app. See the related documentation in [`INSTALL.md` file](INSTALL.md#31-mandatory-app_root_host-variable).
|
||||
|
||||
5. Configure the upcoming Spaces.
|
||||
5. Configure your Spaces.
|
||||
|
||||
6. Remove the instance based environnement variables and configure them directly in the spaces.
|
||||
6. Remove the instance based environnement variables (see **Changed** above) and configure them directly in the spaces using the API or Web Panel.
|
||||
|
||||
7. (Optional) Import the old instance DotEnv environnement variables into a space.
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ php artisan spaces:create-update beta.sip beta.myhost.com "Beta Space"
|
|||
php artisan spaces:import-configuration-from-dot-env {sip_domain}
|
||||
```
|
||||
|
||||
You can find more details regarding those steps in the [`README.md`](README.md) file.
|
||||
You can find more details regarding those steps in the [`INSTALL.md`](INSTALL.md) and [`README.md`](README.md) files.
|
||||
|
||||
### Deprecated
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ MAIL_VERIFY_PEER_NAME=true
|
|||
MAIL_SIGNATURE="The Example Team"
|
||||
|
||||
# CoTURN
|
||||
|
||||
COTURN_SERVER_HOST= # IP or domain name
|
||||
COTURN_SESSION_TTL_MINUTES=1440 # 60 * 24
|
||||
COTURN_STATIC_AUTH_SECRET= # static-auth-secret in the coturn configuration
|
||||
|
|
|
|||
|
|
@ -121,6 +121,11 @@ class Account extends Authenticatable
|
|||
return $this->hasOne(ApiKey::class);
|
||||
}
|
||||
|
||||
public function external()
|
||||
{
|
||||
return $this->hasOne(ExternalAccount::class);
|
||||
}
|
||||
|
||||
public function contacts()
|
||||
{
|
||||
return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id');
|
||||
|
|
@ -324,11 +329,6 @@ class Account extends Authenticatable
|
|||
return null;
|
||||
}
|
||||
|
||||
public function getSha256PasswordAttribute()
|
||||
{
|
||||
return $this->passwords()->where('algorithm', 'SHA-256')->exists();
|
||||
}
|
||||
|
||||
public static function dtmfProtocolsRule()
|
||||
{
|
||||
return implode(',', array_keys(self::$dtmfProtocols));
|
||||
|
|
|
|||
23
flexiapi/app/ExternalAccount.php
Normal file
23
flexiapi/app/ExternalAccount.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ExternalAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const PROTOCOLS = ['UDP', 'TCP','TLS'];
|
||||
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo(Account::class);
|
||||
}
|
||||
|
||||
public function getIdentifierAttribute(): string
|
||||
{
|
||||
return $this->attributes['username'] . '@' . $this->attributes['domain'];
|
||||
}
|
||||
}
|
||||
|
|
@ -20,12 +20,16 @@
|
|||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Account;
|
||||
use App\ExternalAccount;
|
||||
use App\Password;
|
||||
use App\PhoneCountry;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\File;
|
||||
use Propaganistas\LaravelPhone\PhoneNumber;
|
||||
|
||||
class AccountImportController extends Controller
|
||||
{
|
||||
|
|
@ -52,6 +56,7 @@ class AccountImportController extends Controller
|
|||
]);
|
||||
|
||||
$lines = $this->csvToCollection($request->file('csv'));
|
||||
$domain = $request->get('domain');
|
||||
|
||||
/**
|
||||
* Error checking
|
||||
|
|
@ -59,7 +64,7 @@ class AccountImportController extends Controller
|
|||
|
||||
// Usernames
|
||||
|
||||
$existingUsernames = Account::where('domain', $request->get('domain'))
|
||||
$existingUsernames = Account::where('domain', $domain)
|
||||
->whereIn('username', $lines->pluck('username')->all())
|
||||
->pluck('username');
|
||||
|
||||
|
|
@ -100,13 +105,17 @@ class AccountImportController extends Controller
|
|||
if ($lines->pluck('status')->contains(function ($value) {
|
||||
return !in_array($value, ['active', 'inactive']);
|
||||
})) {
|
||||
$this->errors['Some status are not correct'] = '';
|
||||
$this->errors['Some statuses are not correct'] = '';
|
||||
}
|
||||
|
||||
// Phones
|
||||
|
||||
$phoneCountries = PhoneCountry::where('activated', true)->get();
|
||||
|
||||
if ($phones = $lines->pluck('phone')->filter(function ($value) {
|
||||
return strlen($value) > 2 && substr($value, 0, 1) != '+';
|
||||
return !empty($value);
|
||||
})->filter(function ($value) use ($phoneCountries) {
|
||||
return !$phoneCountries->firstWhere('code', (new PhoneNumber($value))->getCountry());
|
||||
})) {
|
||||
if ($phones->isNotEmpty()) {
|
||||
$this->errors['Some phone numbers are not correct'] = $phones->join(', ', ' and ');
|
||||
|
|
@ -143,6 +152,26 @@ class AccountImportController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
// External account
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if ($line->external_username != null && ($line->external_password == null || $line->external_domain == null)) {
|
||||
$this->errors['Line ' . $line->line . ': The mandatory external account columns must be filled'] = '';
|
||||
}
|
||||
|
||||
if ($line->external_username != null && $line->external_password != null && $line->external_domain != null) {
|
||||
if ($line->external_domain == $line->external_realm
|
||||
|| $line->external_domain == $line->external_registrar
|
||||
|| $line->external_domain == $line->external_outbound_proxy) {
|
||||
$this->errors['Line ' . $line->line . ': External realm, registrar or outbound proxy must be different than domain'] = '';
|
||||
}
|
||||
|
||||
if (!in_array($line->external_protocol, ExternalAccount::PROTOCOLS)) {
|
||||
$this->errors['Line ' . $line->line . ': External protocol must be UDP, TCP or TLS'] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filePath = $this->errors->isEmpty()
|
||||
? Storage::putFile($this->importDirectory, $request->file('csv'))
|
||||
: null;
|
||||
|
|
@ -167,7 +196,9 @@ class AccountImportController extends Controller
|
|||
$accounts = [];
|
||||
$now = \Carbon\Carbon::now();
|
||||
|
||||
$admins = $phones = $passwords = [];
|
||||
$admins = $phones = $passwords = $externals = [];
|
||||
|
||||
$externalAlgorithm = 'MD5';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if ($line->role == 'admin') {
|
||||
|
|
@ -182,6 +213,25 @@ class AccountImportController extends Controller
|
|||
$passwords[$line->username] = $line->password;
|
||||
}
|
||||
|
||||
if (!empty($line->external_username)) {
|
||||
$externals[$line->username] = [
|
||||
'username' => $line->external_username,
|
||||
'domain' => $line->external_domain,
|
||||
'realm' => $line->external_realm,
|
||||
'registrar' => $line->external_registrar,
|
||||
'outbound_proxy' => $line->external_outbound_proxy,
|
||||
'protocol' => $line->external_protocol,
|
||||
'password' => bchash(
|
||||
$line->external_username,
|
||||
$line->external_realm ?? $line->external_domain,
|
||||
$line->external_password,
|
||||
$externalAlgorithm
|
||||
),
|
||||
'algorithm' => $externalAlgorithm,
|
||||
'created_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
|
||||
array_push($accounts, [
|
||||
'username' => $line->username,
|
||||
'domain' => $request->get('domain'),
|
||||
|
|
@ -229,7 +279,23 @@ class AccountImportController extends Controller
|
|||
|
||||
Password::insert($passwordsToInsert);
|
||||
|
||||
// Set admins accounts
|
||||
// Set external account
|
||||
|
||||
$externalAccountsToInsert = [];
|
||||
|
||||
$externalAccounts = Account::whereIn('username', array_keys($externals))
|
||||
->where('domain', $request->get('domain'))
|
||||
->get();
|
||||
|
||||
foreach ($externalAccounts as $externalAccount) {
|
||||
array_push($externalAccountsToInsert, [
|
||||
'account_id' => $externalAccount->id
|
||||
] + $externals[$externalAccount->username]);
|
||||
}
|
||||
|
||||
ExternalAccount::insert($externalAccountsToInsert);
|
||||
|
||||
// Set phone accounts
|
||||
foreach ($phones as $username => $phone) {
|
||||
$account = Account::where('username', $username)
|
||||
->where('domain', $request->get('domain'))
|
||||
|
|
@ -250,12 +316,20 @@ class AccountImportController extends Controller
|
|||
if ($line = fgetcsv($csv, 1000, ',')) {
|
||||
$lines->push((object)[
|
||||
'line' => $i,
|
||||
'username' => $line[0],
|
||||
'password' => $line[1],
|
||||
'username' => !empty($line[0]) ? $line[0] : null,
|
||||
'password' => !empty($line[1]) ? $line[1] : null,
|
||||
'role' => $line[2],
|
||||
'status' => $line[3],
|
||||
'phone' => $line[4],
|
||||
'phone' => !empty($line[4]) ? $line[4] : null,
|
||||
'email' => $line[5],
|
||||
'external_username' => !empty($line[6]) ? $line[6] : null,
|
||||
'external_domain' => !empty($line[7]) ? $line[7] : null,
|
||||
'external_password' => !empty($line[8]) ? $line[8] : null,
|
||||
'external_realm' => !empty($line[9]) ? $line[9] : null,
|
||||
'external_registrar' => !empty($line[10]) ? $line[10] : null,
|
||||
'external_outbound_proxy' => !empty($line[11]) ? $line[11] : null,
|
||||
'external_protocol' => $line[12],
|
||||
|
||||
]);
|
||||
|
||||
$i++;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
<?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;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ExternalAccount\CreateUpdate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\ExternalAccount;
|
||||
use App\Account;
|
||||
|
||||
class ExternalAccountController extends Controller
|
||||
{
|
||||
public function show(int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
|
||||
return view('admin.account.external.show', [
|
||||
'account' => $account,
|
||||
'externalAccount' => $account->external ?? new ExternalAccount,
|
||||
'protocols' => ExternalAccount::PROTOCOLS
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CreateUpdate $request, int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
$externalAccount = $account->external ?? new ExternalAccount;
|
||||
|
||||
$password = '';
|
||||
if ($account->external?->realm != $request->get('realm')) {
|
||||
$password = 'required_with:realm';
|
||||
} elseif ($externalAccount->password == null) {
|
||||
$password = 'required';
|
||||
}
|
||||
|
||||
$request->validate(['password' => $password]);
|
||||
|
||||
$algorithm = 'MD5';
|
||||
|
||||
$externalAccount->account_id = $account->id;
|
||||
$externalAccount->username = $request->get('username');
|
||||
$externalAccount->domain = $request->get('domain');
|
||||
$externalAccount->realm = $request->get('realm');
|
||||
$externalAccount->registrar = $request->get('registrar');
|
||||
$externalAccount->outbound_proxy = $request->get('outbound_proxy');
|
||||
$externalAccount->protocol = $request->get('protocol');
|
||||
|
||||
if (!empty($request->get('password'))) {
|
||||
$externalAccount->password = bchash(
|
||||
$externalAccount->username,
|
||||
$externalAccount->realm ?? $externalAccount->domain,
|
||||
$request->get('password'),
|
||||
$algorithm
|
||||
);
|
||||
$externalAccount->algorithm = $algorithm;
|
||||
}
|
||||
|
||||
$externalAccount->save();
|
||||
|
||||
return redirect()->route('admin.account.external.show', $account->id);
|
||||
}
|
||||
|
||||
public function delete(int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
|
||||
return view('admin.account.external.delete', [
|
||||
'account' => $account
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
$account->external->delete();
|
||||
|
||||
return redirect()->route('admin.account.external.show', $account->id);
|
||||
}
|
||||
}
|
||||
|
|
@ -147,16 +147,6 @@ class AccountController extends Controller
|
|||
|
||||
public function store(AsAdminRequest $request)
|
||||
{
|
||||
// Create the missing Space
|
||||
/*if ($request->user()->superAdmin
|
||||
&& $request->has('domain')
|
||||
&& !Space::pluck('domain')->contains($request->get('domain'))) {
|
||||
$space = new Space();
|
||||
$space->domain = $request->get('domain');
|
||||
$space->host = $request->get('host');
|
||||
$space->save();
|
||||
}*/
|
||||
|
||||
return (new AccountService())->store($request)->makeVisible(['confirmation_key', 'provisioning_token']);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
<?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\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ExternalAccount\CreateUpdate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\ExternalAccount;
|
||||
use App\Account;
|
||||
|
||||
class ExternalAccountController extends Controller
|
||||
{
|
||||
public function show(int $accountId)
|
||||
{
|
||||
return Account::findOrFail($accountId)->external()->firstOrFail();
|
||||
}
|
||||
|
||||
public function store(CreateUpdate $request, int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
$externalAccount = $account->external ?? new ExternalAccount;
|
||||
|
||||
$password = '';
|
||||
if ($account->external?->realm != $request->get('realm')) {
|
||||
$password = 'required_with:realm';
|
||||
} elseif ($externalAccount->password == null) {
|
||||
$password = 'required';
|
||||
}
|
||||
|
||||
$request->validate(['password' => $password]);
|
||||
|
||||
$algorithm = 'MD5';
|
||||
|
||||
$externalAccount->account_id = $account->id;
|
||||
$externalAccount->username = $request->get('username');
|
||||
$externalAccount->domain = $request->get('domain');
|
||||
$externalAccount->realm = $request->get('realm');
|
||||
$externalAccount->registrar = $request->get('registrar');
|
||||
$externalAccount->outbound_proxy = $request->get('outbound_proxy');
|
||||
$externalAccount->protocol = $request->get('protocol');
|
||||
$externalAccount->algorithm = $algorithm;
|
||||
|
||||
if (!empty($request->get('password'))) {
|
||||
$externalAccount->password = bchash(
|
||||
$externalAccount->username,
|
||||
$externalAccount->realm ?? $externalAccount->domain,
|
||||
$request->get('password'),
|
||||
$algorithm
|
||||
);
|
||||
}
|
||||
|
||||
$externalAccount->save();
|
||||
|
||||
return $externalAccount;
|
||||
}
|
||||
|
||||
public function destroy(int $accountId)
|
||||
{
|
||||
$account = Account::findOrFail($accountId);
|
||||
return $account->external->delete();
|
||||
}
|
||||
}
|
||||
43
flexiapi/app/Http/Requests/ExternalAccount/CreateUpdate.php
Normal file
43
flexiapi/app/Http/Requests/ExternalAccount/CreateUpdate.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?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\ExternalAccount;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\ExternalAccount;
|
||||
|
||||
class CreateUpdate extends FormRequest
|
||||
{
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'username' => 'required',
|
||||
'domain' => 'required',
|
||||
'realm' => 'different:domain',
|
||||
'registrar' => 'different:domain',
|
||||
'outbound_proxy' => 'different:domain',
|
||||
'protocol' => [
|
||||
'required',
|
||||
Rule::in(ExternalAccount::PROTOCOLS),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ class StatisticsGraphFactory
|
|||
$toQuery = StatisticsMessage::query();
|
||||
|
||||
if (!Auth::user()?->isAdmin) {
|
||||
$fromQuery->where('from_domain', config('app.sip_domain'));
|
||||
$fromQuery->where('from_domain', space()->domain);
|
||||
$toQuery->toDomain($this->domain);
|
||||
} elseif ($this->domain) {
|
||||
$fromQuery->where('from_domain', $this->domain);
|
||||
|
|
@ -90,8 +90,8 @@ class StatisticsGraphFactory
|
|||
$toQuery = StatisticsCall::query();
|
||||
|
||||
if (!Auth::user()?->superAdmin) {
|
||||
$fromQuery->where('from_domain', config('app.sip_domain'));
|
||||
$toQuery->where('to_domain', config('app.sip_domain'));
|
||||
$fromQuery->where('from_domain', space()->domain);
|
||||
$toQuery->where('to_domain', space()->domain);
|
||||
} elseif ($this->domain) {
|
||||
$fromQuery = $fromQuery->where('to_domain', $this->domain);
|
||||
$toQuery = $toQuery->where('from_domain', $this->domain);
|
||||
|
|
@ -127,7 +127,7 @@ class StatisticsGraphFactory
|
|||
$this->domain = $this->domain ?? $this->fromDomain;
|
||||
|
||||
if (!Auth::user()?->isAdmin) {
|
||||
$this->data->where('domain', config('app.sip_domain'));
|
||||
$this->data->where('domain', space()->domain);
|
||||
} elseif ($this->domain) {
|
||||
$this->data->where('domain', $this->domain);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class AccountService
|
|||
$account = new Account();
|
||||
$account->username = $request->get('username');
|
||||
$account->activated = false;
|
||||
$account->domain = config('app.sip_domain');
|
||||
$account->domain = space()->domain;
|
||||
$account->ip_address = $request->ip();
|
||||
$account->created_at = Carbon::now();
|
||||
$account->user_agent = space()->name;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?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('external_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('username', 64);
|
||||
$table->string('domain', 64);
|
||||
$table->string('password', 255);
|
||||
$table->string('algorithm', 10)->default('MD5');
|
||||
$table->string('realm', 64)->nullable();
|
||||
$table->string('registrar', 64)->nullable();
|
||||
$table->string('outbound_proxy', 64)->nullable();
|
||||
$table->string('protocol', 4)->default('UDP');
|
||||
|
||||
$table->integer('account_id')->unsigned()->nullable();
|
||||
$table->foreign('account_id')->references('id')
|
||||
->on('accounts')->onDelete('set null');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('external_accounts');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
{
|
||||
"A verification code was sent by email to :email.": "Un code de vérification a été envoyé par email à :email",
|
||||
"A verification code was sent by SMS to :phone.": "Un code de vérification a été envoyé par SMS au :phone",
|
||||
"About": "À Propos",
|
||||
"Account creation": "Création de compte",
|
||||
"Account recovered recently, try again later": "Tentative de récupération de compte récente, retentez ultérieurement",
|
||||
"Account recovery": "Récupération de compte",
|
||||
"Account settings": "Paramètres de compte",
|
||||
"Account": "Compte",
|
||||
"Accounts": "Comptes",
|
||||
"Actions": "Actions",
|
||||
"Activate All": "Tout activer",
|
||||
|
|
@ -18,28 +21,26 @@
|
|||
"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 client settings to be overwritten by the provisioning ones": "Écraser la configuration client avec celle du provisionnement",
|
||||
"An email will be sent to this email when someone join the newsletter": "Un email sera envoyé à cette addresse quand quelqu'un rejoint la liste de diffusion",
|
||||
"An email will be sent to :email with a unique link allowing the user to reset its password.": "Un email sera envoyé à :email avec un lien unique l'invitant à réinitialiser son mot de passe",
|
||||
"An email will be sent to this email when someone join the newsletter": "Un email sera envoyé à cette addresse quand quelqu'un rejoint la liste de diffusion",
|
||||
"App Configuration": "Configuration de l'App",
|
||||
"A verification code was sent by SMS to :phone.": "Un code de vérification a été envoyé par SMS au :phone",
|
||||
"A verification code was sent by email to :email.": "Un code de vérification a été envoyé par email à :email",
|
||||
"Assistant": "Assistant",
|
||||
"Blocked": "Bloqué",
|
||||
"Calls logs": "Journaux d'appel",
|
||||
"Cancel": "Annuler",
|
||||
"Cannot be changed once created.": "Ne peut être changé par la suite.",
|
||||
"Chat": "Chat",
|
||||
"Change your phone number": "Changer votre numéro de téléphone",
|
||||
"Change your email": "Changer votre email",
|
||||
"Change your phone number": "Changer votre numéro de téléphone",
|
||||
"Chat": "Chat",
|
||||
"Check the README.md documentation": "Voir la documentation dans README.md",
|
||||
"Clear to never expire": "Laisser vide pour ne jamais expirer",
|
||||
"Code": "Code",
|
||||
"Connection": "Connexion",
|
||||
"Conference": "Conférence",
|
||||
"Configuration": "Configuration",
|
||||
"Confirm email": "Confirmation de l'email",
|
||||
"Confirm password": "Confirmer le mot de passe",
|
||||
"Confirmed registration text": "Texte de confirmation d'inscription",
|
||||
"Connection": "Connexion",
|
||||
"Contacts List": "Liste de Contacts",
|
||||
"Contacts Lists": "Listes de Contacts",
|
||||
"Contacts": "Contacts",
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"Country code": "Code du pays",
|
||||
"Create": "Créer",
|
||||
"Created on": "Créé le",
|
||||
"Currently set": "Actuellement remplit",
|
||||
"Custom entries": "Entrées personnalisées",
|
||||
"Dactivate": "Désactiver",
|
||||
"Day": "Jour",
|
||||
|
|
@ -64,17 +66,21 @@
|
|||
"Empty": "Vide",
|
||||
"Enable the web interface": "Activer l'interface web",
|
||||
"Enabled": "Activé",
|
||||
"Encrypted": "Chiffré",
|
||||
"Enter the pin code bellow:": "Entrez le code pin ci-dessous:",
|
||||
"Errors": "Erreurs",
|
||||
"Expiration": "Expiration",
|
||||
"Export": "Exporter",
|
||||
"Expired": "Expiré",
|
||||
"Export": "Exporter",
|
||||
"External Account": "Compte Externe",
|
||||
"Features": "Fonctionnalités",
|
||||
"Fill to change": "Remplir pour changer",
|
||||
"Fill the related columns if you want to add an external account as well": "Remplissez également les colonnes suivantes si vous souhaitez ajouter un compte externe",
|
||||
"From": "Depuis",
|
||||
"General settings": "Paramètres généraux",
|
||||
"Host": "Hôte",
|
||||
"I accept the Terms and Conditions": "J'accepte les Conditions Générales",
|
||||
"I accept the Privacy Policy": "J'accepte la Politique de Confidentialité",
|
||||
"I accept the Terms and Conditions": "J'accepte les Conditions Générales",
|
||||
"I would like to subscribe to the newsletter": "Je voudrais m'inscrire à la newsletter",
|
||||
"I'm not a robot": "Je ne suis pas un robot",
|
||||
"Identifier": "Identifiant",
|
||||
|
|
@ -106,44 +112,50 @@
|
|||
"No limit": "Sans limite",
|
||||
"No phone yet": "Pas de téléphone pour le moment",
|
||||
"Only display usernames (hide SIP addresses)": "N'afficher que les num d'utilisateur (cacher les addresses SIP)",
|
||||
"Other information": "Autres informations",
|
||||
"Outbound proxy": "Outbound proxy",
|
||||
"Password": "Mot de passe",
|
||||
"Phone Countries": "Numéros Internationaux",
|
||||
"Phone number": "Numéro de téléphone",
|
||||
"Phone registration": "Inscription via mobile",
|
||||
"Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.",
|
||||
"Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.",
|
||||
"Protocol": "Protocole",
|
||||
"Provisioning tokens": "Jetons de provisionnement",
|
||||
"Provisioning": "Provisionnement",
|
||||
"Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.",
|
||||
"Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.",
|
||||
"Public registration": "Inscription publiques",
|
||||
"QR Code scanning": "Scan de QR Code",
|
||||
"Realm": "Royaume",
|
||||
"Record calls": "Enregistrements des appels",
|
||||
"Recover your account using your email": "Récupérer votre compte avec votre email",
|
||||
"Recover your account using your phone number": "Récupérer votre compte avec votre numéro de téléphone",
|
||||
"Register": "Inscription",
|
||||
"Registrar": "Registrar",
|
||||
"Registration introduction": "Présentation lors de l'inscription",
|
||||
"Remove": "Remove",
|
||||
"Renew": "Renouveller",
|
||||
"Requests": "Requêtes",
|
||||
"Resend": "Ré-envoyer",
|
||||
"Reset password": "Réinitialiser le mot de passe",
|
||||
"Reset password emails": "Email de réinitialisation de mot de passe",
|
||||
"Reset password": "Réinitialiser le mot de passe",
|
||||
"Reset": "Réinitialiser",
|
||||
"Role": "Rôle",
|
||||
"Scan the following QR Code using an authenticated device and wait a few seconds.": "Scanner le QR Code avec un appareil authentifié et attendez quelques secondes",
|
||||
"Search": "Rechercher",
|
||||
"Select a contacts list": "Sélectionner une liste de contact",
|
||||
"Select a domain": "Sélectionner un domaine",
|
||||
"Select a file": "Choisir un fichier",
|
||||
"Send": "Envoyer",
|
||||
"Send an email to the user to reset the password": "Envoyer un email à l'utilisateur pour réinitialiser son mot de passe",
|
||||
"Send": "Envoyer",
|
||||
"Sip Adress": "Adresse SIP",
|
||||
"SIP Domain": "Domaine SIP",
|
||||
"Space": "Espace",
|
||||
"Spaces": "Espaces",
|
||||
"Statistics": "Statistiques",
|
||||
"Scan the following QR Code using an authenticated device and wait a few seconds.": "Scanner le QR Code avec un appareil authentifié et attendez quelques secondes",
|
||||
"Subdomain": "Sous-domaine",
|
||||
"Super Admin": "Super Admin",
|
||||
"Super Space": "Super Espace",
|
||||
"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 account doesn't exists": "Le compte n'existe pas",
|
||||
"The code has expired": "Le code a expiré",
|
||||
|
|
@ -154,10 +166,9 @@
|
|||
"The first line contains the labels": "La premières ligne contient les étiquettes",
|
||||
"The link can only be visited once": "Le lien ne peut être utilisé qu'une fois",
|
||||
"Third party SIP": "Adresse SIP tierce",
|
||||
"This link will be available for :hours hours.": "Ce lien restera disponible pour :hours heures.",
|
||||
"This link is not available anymore.": "Ce lien n'est plus disponible.",
|
||||
"This link will be available for :hours hours.": "Ce lien restera disponible pour :hours heures.",
|
||||
"To": "À",
|
||||
"Thanks for the validation": "Nous vous remercions pour la validation",
|
||||
"Transport": "Transport",
|
||||
"Types": "Types",
|
||||
"Unlimited if set to 0": "Illimité si à 0",
|
||||
|
|
@ -174,10 +185,10 @@
|
|||
"Welcome on :app_name": "Bienvenue sur :app_name",
|
||||
"Wrong username or password": "Mauvais identifiant ou mot de passe",
|
||||
"Year": "Année",
|
||||
"You didn't receive the code?": "Vous n'avez pas reçu le code ?",
|
||||
"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 can now continue your registration process in the application": "Vous pouvez maintenant continuer le processus d'inscription dans l'application",
|
||||
"You are going to permanently delete your account. Please enter your complete SIP address to confirm.": "Vous allez détruire défitivement votre compte. Veuillez entrer votre addresse SIP complète pour confirmer.",
|
||||
"You can now continue your registration process in the application": "Vous pouvez maintenant continuer le processus d'inscription dans l'application",
|
||||
"You didn't receive the code?": "Vous n'avez pas reçu le code ?",
|
||||
"Your password was updated properly.": "Votre mot de passe a été mis à jour."
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Username,Password,Role,Status,Phone,Email
|
||||
john,number9,user,active,+12341234,john@lennon.com
|
||||
paul,a_day_in_the_life,admin,active,,paul@apple.com
|
||||
ringo,allUneedIsL3ve,user,unactive,+123456,ringo@star.co.uk
|
||||
Username,Password,Role,Status,Phone,Email,External Username,External Domain,External Password,External Realm, External Registrar,External Outbound Proxy,External Encrypted,External Protocol
|
||||
john,number9,user,active,+12341234,john@lennon.com,extjohn,ext.lennon.com,123ext,,,,UDP
|
||||
paul,a_day_in_the_life,admin,active,,paul@apple.com,,,,,,,
|
||||
ringo,allUneedIsL3ve,user,inactive,+123456,ringo@star.co.uk,extringo,ext.star.co.uk,123456,another.realm,,,UDP
|
||||
|
3
flexiapi/public/css/form.css
vendored
3
flexiapi/public/css/form.css
vendored
|
|
@ -85,7 +85,8 @@ p .btn {
|
|||
border-color: var(--main-3);
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
form section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem 2.5rem;
|
||||
|
|
|
|||
29
flexiapi/public/css/style.css
vendored
29
flexiapi/public/css/style.css
vendored
|
|
@ -342,6 +342,13 @@ content section {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form section {
|
||||
margin: initial;
|
||||
max-width: initial;
|
||||
padding: 0;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
|
@ -533,6 +540,7 @@ h3 {
|
|||
color: var(--second-6);
|
||||
padding: 0.5rem 0;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--grey-2);
|
||||
}
|
||||
|
|
@ -908,4 +916,23 @@ ol.steps li.active:before {
|
|||
background-color: var(--main-5);
|
||||
border-color: var(--main-5);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
/** Show/hide toggle **/
|
||||
|
||||
details > summary::before {
|
||||
content: "▶";
|
||||
color: var(--grey-4);
|
||||
font-size: 1.5rem;
|
||||
line-height: 4rem;
|
||||
padding: 0 1rem;
|
||||
float: right;
|
||||
}
|
||||
|
||||
details[open] > summary::before {
|
||||
content: "▼";
|
||||
}
|
||||
|
||||
details > summary:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
@include('admin.account.parts.breadcrumb_accounts_index')
|
||||
@if ($account->id)
|
||||
@include('admin.account.parts.breadcrumb_accounts_edit', ['account' => $account])
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ __('Edit') }}</li>
|
||||
@else
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ __('Create') }}</li>
|
||||
@endif
|
||||
|
|
@ -14,12 +13,10 @@
|
|||
@if ($account->id)
|
||||
<header>
|
||||
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
|
||||
<a href="{{ route('admin.account.index') }}" class="btn btn-secondary oppose">{{ __('Cancel') }}</a>
|
||||
<a class="btn btn-secondary" href="{{ route('admin.account.delete', $account->id) }}">
|
||||
<a class="btn btn-secondary oppose" href="{{ route('admin.account.delete', $account->id) }}">
|
||||
<i class="ph">trash</i>
|
||||
{{ __('Delete') }}
|
||||
</a>
|
||||
<input form="create_edit" class="btn" type="submit" value="{{ __('Update') }}">
|
||||
</header>
|
||||
@if ($account->updated_at)
|
||||
<p title="{{ $account->updated_at }}">{{ __('Updated on') }} {{ $account->updated_at->format('d/m/Y') }}
|
||||
|
|
@ -38,7 +35,7 @@
|
|||
id="create_edit" accept-charset="UTF-8">
|
||||
@csrf
|
||||
@method($account->id ? 'put' : 'post')
|
||||
<h2>{{ __('Connection') }}</h2>
|
||||
|
||||
<div>
|
||||
<input placeholder="Username" required="required" name="username" type="text"
|
||||
value="@if($account->id){{ $account->username }}@else{{ old('username') }}@endif"
|
||||
|
|
@ -67,8 +64,8 @@
|
|||
<div>
|
||||
<input placeholder="Password" name="password" type="password" value="" autocomplete="new-password"
|
||||
@if (!$account->id) required @endif>
|
||||
<label for="password">{{ __('Password') }}</label>
|
||||
<small>Fill to change</small>
|
||||
<label for="password">{{ __('Password') }} @if ($account->passwords()->count() > 0) ({{ __('Currently set') }}) @endif</label>
|
||||
<small>{{ __('Fill to change') }}</small>
|
||||
@include('parts.errors', ['name' => 'password'])
|
||||
</div>
|
||||
|
||||
|
|
@ -101,11 +98,10 @@
|
|||
@include('parts.errors', ['name' => 'phone'])
|
||||
</div>
|
||||
|
||||
<h2>Other information</h2>
|
||||
<h3 class="large">{{ __('Other information') }}</h3>
|
||||
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $account, 'key' => 'blocked', 'label' => __('Blocked')])
|
||||
</div>
|
||||
@include('parts.form.toggle', ['object' => $account, 'key' => 'blocked', 'label' => __('Blocked')])
|
||||
@include('parts.form.toggle', ['object' => $account, 'key' => 'activated', 'label' => __('Enabled')])
|
||||
|
||||
<div>
|
||||
<input name="role" value="admin" type="radio" @if ($account->admin) checked @endif>
|
||||
|
|
@ -115,8 +111,6 @@
|
|||
<label>{{ __('Role') }}</label>
|
||||
</div>
|
||||
|
||||
@include('parts.form.toggle', ['object' => $account, 'key' => 'activated', 'label' => __('Enabled')])
|
||||
|
||||
@if (space()?->intercom_features)
|
||||
<div class="select">
|
||||
<select name="dtmf_protocol">
|
||||
|
|
@ -129,12 +123,28 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<div class="large">
|
||||
<input class="btn" type="submit" value="{{ __('Update') }}">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="large">
|
||||
|
||||
@if ($account->id)
|
||||
<h2 id="contacts_lists">{{ __('Contacts Lists') }}</h2>
|
||||
<h2 class="large">{{ __('Contacts') }}</h2>
|
||||
|
||||
@foreach ($account->contacts as $contact)
|
||||
<p class="chip">
|
||||
<a href="{{ route('admin.account.edit', $account) }}">{{ $contact->identifier }}</a>
|
||||
<a href="{{ route('admin.account.contact.delete', [$account, $contact->id]) }}">
|
||||
<i class="ph">x</i>
|
||||
</a>
|
||||
</p>
|
||||
@endforeach
|
||||
|
||||
<a class="btn btn-tertiary" href="{{ route('admin.account.contact.create', $account) }}">{{ __('Add') }}</a>
|
||||
|
||||
<h3 id="contacts_lists">{{ __('Contacts Lists') }}</h3>
|
||||
|
||||
@if ($contacts_lists->isNotEmpty())
|
||||
<form method="POST" action="{{ route('admin.account.contacts_lists.attach', $account->id) }}"
|
||||
|
|
@ -170,26 +180,14 @@
|
|||
</p>
|
||||
@endforeach
|
||||
|
||||
<h2>{{ __('Contacts') }}</h2>
|
||||
|
||||
@foreach ($account->contacts as $contact)
|
||||
<p class="chip">
|
||||
<a href="{{ route('admin.account.edit', $account) }}">{{ $contact->identifier }}</a>
|
||||
<a href="{{ route('admin.account.contact.delete', [$account, $contact->id]) }}">
|
||||
<i class="ph">x</i>
|
||||
</a>
|
||||
</p>
|
||||
@endforeach
|
||||
|
||||
<br />
|
||||
<a class="btn btn-tertiary" href="{{ route('admin.account.contact.create', $account) }}">{{ __('Add') }}</a>
|
||||
|
||||
<hr class="large">
|
||||
|
||||
<h2 id="provisioning">{{ __('Provisioning') }}</h2>
|
||||
<h2 class="large" id="provisioning">{{ __('Provisioning') }}</h2>
|
||||
|
||||
@if ($account->provisioning_token)
|
||||
<img style="max-width: 15rem;" src="{{ route('provisioning.qrcode', $account->provisioning_token) }}">
|
||||
<div>
|
||||
<img style="max-width: 15rem;" src="{{ route('provisioning.qrcode', $account->provisioning_token) }}">
|
||||
</div>
|
||||
|
||||
<form class="inline">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
<header>
|
||||
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
|
||||
<a href="{{ route('admin.account.edit', $account->id) }}" class="btn btn-secondary oppose">{{ __('Cancel') }}</a>
|
||||
</header>
|
||||
|
||||
@include('admin.account.parts.tabs')
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@
|
|||
|
||||
<header>
|
||||
<h1><i class="ph">users</i> {{ $account->identifier }}</h1>
|
||||
<a href="{{ route('admin.account.edit', $account->id) }}" class="btn btn-secondary oppose">{{ __('Cancel') }}</a>
|
||||
<a class="btn" href="{{ route('admin.account.dictionary.create', $account) }}">
|
||||
<a class="btn oppose" href="{{ route('admin.account.dictionary.create', $account) }}">
|
||||
<i class="ph">plus</i>
|
||||
{{ __('Add') }}
|
||||
</a>
|
||||
|
|
|
|||
30
flexiapi/resources/views/admin/account/external/delete.blade.php
vendored
Normal file
30
flexiapi/resources/views/admin/account/external/delete.blade.php
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumb')
|
||||
@include('admin.account.parts.breadcrumb_accounts_index')
|
||||
@include('admin.account.parts.breadcrumb_accounts_edit', ['account' => $account])
|
||||
<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>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<header>
|
||||
<h1><i class="ph">trash</i> {{ __('Delete') }}</h1>
|
||||
|
||||
<a href="{{ route('admin.account.external.show', ['account' => $account]) }}" class="btn btn-secondary oppose">{{ __('Cancel') }}</a>
|
||||
<input form="delete" class="btn" type="submit" value="{{ __('Delete') }}">
|
||||
</header>
|
||||
<form id="delete" method="POST" action="{{ route('admin.account.external.destroy', $account->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>{{ $account->external->identifier }}</b>
|
||||
</p>
|
||||
<input name="account_id" type="hidden" value="{{ $account->id }}">
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
86
flexiapi/resources/views/admin/account/external/show.blade.php
vendored
Normal file
86
flexiapi/resources/views/admin/account/external/show.blade.php
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('breadcrumb')
|
||||
@include('admin.account.parts.breadcrumb_accounts_index')
|
||||
@include('admin.account.parts.breadcrumb_accounts_edit', ['account' => $account])
|
||||
<li class="breadcrumb-item active">{{ __('External Account') }}</li>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<header>
|
||||
<h1><i class="ph">user-circle-dashed</i> {{ __('External Account') }}</h1>
|
||||
@if($externalAccount->id)
|
||||
<a class="btn btn-secondary oppose" href="{{ route('admin.account.external.delete', $account->id) }}">
|
||||
<i class="ph">trash</i>
|
||||
{{ __('Delete') }}
|
||||
</a>
|
||||
@endif
|
||||
</header>
|
||||
@include('admin.account.parts.tabs')
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('admin.account.external.store', $account->id) }}"
|
||||
id="show" accept-charset="UTF-8">
|
||||
@csrf
|
||||
@method('post')
|
||||
<h3 class="large">{{ __('Connection') }}</h3>
|
||||
<div>
|
||||
<input placeholder="username" required="required" name="username" type="text"
|
||||
value="@if($externalAccount->id){{ $externalAccount->username }}@else{{ old('username') }}@endif">
|
||||
<label for="username">{{ __('Username') }}</label>
|
||||
@include('parts.errors', ['name' => 'username'])
|
||||
</div>
|
||||
<div>
|
||||
<input placeholder="domain.tld" required="required" name="domain" type="text"
|
||||
value="@if($externalAccount->id){{ $externalAccount->domain }}@else{{ old('domain') }}@endif">
|
||||
<label for="domain">{{ __('Domain') }}</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input placeholder="Password" name="password" type="password" value="" autocomplete="new-password"
|
||||
@if (!$externalAccount->id) required @endif>
|
||||
<label for="password">{{ __('Password') }} @if ($externalAccount->id) ({{ __('Currently set') }}) @endif</label>
|
||||
@if($externalAccount->id)<small>{{ __('Fill to change') }}</small>@endif
|
||||
@include('parts.errors', ['name' => 'password'])
|
||||
</div>
|
||||
|
||||
<details class="large" @if ($errors->isNotEmpty())open @endif>
|
||||
<summary>
|
||||
<h3 class="large">
|
||||
{{ __('Other information') }}
|
||||
</h3>
|
||||
</summary>
|
||||
<section>
|
||||
<div>
|
||||
<input placeholder="realm" name="realm" type="text"
|
||||
value="@if($externalAccount->id){{ $externalAccount->realm }}@else{{ old('realm') }}@endif">
|
||||
<label for="username">{{ __('Realm') }}</label>
|
||||
@include('parts.errors', ['name' => 'realm'])
|
||||
</div>
|
||||
<div>
|
||||
<input placeholder="domain.tld" name="registrar" type="text"
|
||||
value="@if($externalAccount->id){{ $externalAccount->registrar }}@else{{ old('registrar') }}@endif">
|
||||
<label for="domain">{{ __('Registrar') }}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input placeholder="outbound.tld" name="outbound_proxy" type="text"
|
||||
value="@if($externalAccount->id){{ $externalAccount->outbound_proxy }}@else{{ old('outbound_proxy') }}@endif">
|
||||
<label for="domain">{{ __('Outbound Proxy') }}</label>
|
||||
</div>
|
||||
<div class="select">
|
||||
<select name="protocol">
|
||||
@foreach ($protocols as $protocol)
|
||||
<option value="{{ $protocol }}" @if ($externalAccount->protocol == $protocol) selected="selected" @endif>
|
||||
{{ $protocol }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<label for="dtmf_protocol">{{ __('Protocol') }}</label>
|
||||
</div>
|
||||
</section>
|
||||
</details>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<input form="show" class="btn" type="submit" value="@if($externalAccount->id){{ __('Update') }}@else{{ __('Create') }}@endif">
|
||||
@endsection
|
||||
|
|
@ -30,16 +30,14 @@
|
|||
<li class="active">{{ __('Import') }}</li>
|
||||
</ol>
|
||||
|
||||
<h3>{{ $linesCount }} accounts will be imported for the {{ $domain }} domain</h3>
|
||||
|
||||
@if ($errors->isNotEmpty())
|
||||
<hr />
|
||||
|
||||
<h3>{{ __('Errors') }}</h3>
|
||||
|
||||
@foreach ($errors as $title => $body)
|
||||
<p><b>{{ $title }}</b> {{ $body }}</p>
|
||||
@endforeach
|
||||
@else
|
||||
<h3>{{ $linesCount }} accounts will be imported for the {{ $domain }} domain</h3>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@
|
|||
</ol>
|
||||
|
||||
<p>{{ __('The file must be in CSV following this template') }}: <a href="{{ route('account.home') }}/accounts_example.csv">example_template.csv</a></p>
|
||||
|
||||
<h4>{{ __('Account') }}</h4>
|
||||
<p>{{ __('The first line contains the labels') }}</p>
|
||||
<ol>
|
||||
<li>{{ __('The first line contains the labels') }}</li>
|
||||
<li>{{ __('Username') }}* </li>
|
||||
<li>{{ __('Password') }}* (6 characters minimum)</li>
|
||||
<li>{{ __('Role') }}* (admin or user)</li>
|
||||
|
|
@ -29,6 +31,20 @@
|
|||
<li>{{ __('Email') }}</li>
|
||||
</ol>
|
||||
|
||||
<h4>{{ __('External Account') }}</h4>
|
||||
|
||||
<p>{{ __('Fill the related columns if you want to add an external account as well') }}</p>
|
||||
<ol>
|
||||
<li>{{ __('Username') }}* </li>
|
||||
<li>{{ __('Domain') }}* </li>
|
||||
<li>{{ __('Password') }}*</li>
|
||||
<li>{{ __('Realm') }} (different than domain)</li>
|
||||
<li>{{ __('Registrar') }} (different than domain)</li>
|
||||
<li>{{ __('Outbound Proxy') }} (different than domain)</li>
|
||||
<li>{{ __('Encrypted') }} (yes or no)</li>
|
||||
<li>{{ __('Protocol') }} (UDP, TCP or TLS)</li>
|
||||
</ol>
|
||||
|
||||
<hr />
|
||||
|
||||
<form id="import" method="POST" action="{{ route('admin.account.import.store') }}" accept-charset="UTF-8" enctype="multipart/form-data">
|
||||
|
|
|
|||
|
|
@ -90,18 +90,15 @@
|
|||
</td>
|
||||
<td>
|
||||
@if ($account->activated)
|
||||
<span class="badge badge-success" title="Activated">Act.</span>
|
||||
<span class="badge badge-success" title="{{ __('Activated') }}"><i class="ph">check</i></span>
|
||||
@endif
|
||||
@if ($account->superAdmin)
|
||||
<span class="badge badge-error" title="Admin">Super Adm.</span>
|
||||
<span class="badge badge-error" title="{{ __('Super Admin') }}">Super Adm.</span>
|
||||
@elseif ($account->admin)
|
||||
<span class="badge badge-primary" title="Admin">Adm.</span>
|
||||
@endif
|
||||
@if ($account->sha256Password)
|
||||
<span class="badge badge-info">SHA256</span>
|
||||
<span class="badge badge-primary" title="{{ __('Admin') }}">Adm.</span>
|
||||
@endif
|
||||
@if ($account->blocked)
|
||||
<span class="badge badge-error">{{ __('Blocked') }}</span>
|
||||
<span class="badge badge-error" title="{{ __('Blocked') }}"><i class="ph">prohibit</i></span>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $account->updated_at }}</td>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@include('parts.tabs', [
|
||||
'items' => [
|
||||
route('admin.account.edit', $account->id) => __('Information'),
|
||||
route('admin.account.external.show', $account->id) => __('External Account'),
|
||||
route('admin.account.statistics.show_call_logs', $account->id) => __('Calls logs'),
|
||||
route('admin.account.device.index', $account->id) => __('Devices'),
|
||||
route('admin.account.statistics.show', $account->id) => __('Statistics'),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
{{ __('Activate All') }}
|
||||
</a>
|
||||
<a class="btn btn-secondary" href="{{ route('admin.phone_countries.deactivate_all') }}">
|
||||
<i class="ph">trash</i>
|
||||
<i class="ph">minus</i>
|
||||
{{ __('Deactivate All') }}
|
||||
</a>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -677,10 +677,32 @@ JSON parameters:
|
|||
|
||||
* `value` **required**, the entry value
|
||||
|
||||
### `DELETE /accounts/{id}/dictionary/{key}`
|
||||
## External Account
|
||||
|
||||
### `GET /accounts/{id}/external`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
|
||||
Remove an entry from the dictionary.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ 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\ExternalAccountController;
|
||||
use App\Http\Controllers\Api\Admin\SpaceController;
|
||||
use App\Http\Controllers\Api\Admin\VcardsStorageController as AdminVcardsStorageController;
|
||||
use App\Http\Controllers\Api\StatisticsMessageController;
|
||||
|
|
@ -163,6 +164,12 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
|
|||
Route::delete('{key}', 'destroy');
|
||||
});
|
||||
|
||||
Route::prefix('accounts/{id}/external')->controller(ExternalAccountController::class)->group(function () {
|
||||
Route::get('/', 'show');
|
||||
Route::post('/', 'store');
|
||||
Route::delete('/', 'destroy');
|
||||
});
|
||||
|
||||
Route::prefix('statistics/messages')->controller(StatisticsMessageController::class)->group(function () {
|
||||
Route::post('/', 'store');
|
||||
Route::patch('{message_id}/to/{to}/devices/{device_id}', 'storeDevice');
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ use App\Http\Controllers\Admin\AccountController as AdminAccountController;
|
|||
use App\Http\Controllers\Admin\AccountStatisticsController;
|
||||
use App\Http\Controllers\Admin\ContactsListController;
|
||||
use App\Http\Controllers\Admin\ContactsListContactController;
|
||||
use App\Http\Controllers\Admin\ExternalAccountController;
|
||||
use App\Http\Controllers\Admin\PhoneCountryController;
|
||||
use App\Http\Controllers\Admin\ResetPasswordEmailController;
|
||||
use App\Http\Controllers\Admin\SpaceController;
|
||||
|
|
@ -265,6 +266,13 @@ Route::middleware(['web_panel_enabled', 'space.expired'])->group(function () {
|
|||
Route::delete('/', 'destroy')->name('destroy');
|
||||
});
|
||||
|
||||
Route::name('external.')->prefix('{account}/external')->controller(ExternalAccountController::class)->group(function () {
|
||||
Route::get('/', 'show')->name('show');
|
||||
Route::post('/', 'store')->name('store');
|
||||
Route::get('delete', 'delete')->name('delete');
|
||||
Route::delete('/', 'destroy')->name('destroy');
|
||||
});
|
||||
|
||||
Route::name('activity.')->prefix('{account}/activity')->controller(AccountActivityController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
});
|
||||
|
|
|
|||
100
flexiapi/tests/Feature/ApiAccountExternalAccountTest.php
Normal file
100
flexiapi/tests/Feature/ApiAccountExternalAccountTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
/*
|
||||
Flexisip Account Manager is a set of tools to manage SIP accounts.
|
||||
Copyright (C) 2020 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;
|
||||
|
||||
class ApiAccountExternalAccountTest extends TestCase
|
||||
{
|
||||
protected $route = '/api/accounts';
|
||||
protected $method = 'POST';
|
||||
|
||||
public function testCreate()
|
||||
{
|
||||
$account = Account::factory()->create();
|
||||
$admin = Account::factory()->admin()->create();
|
||||
$admin->generateApiKey();
|
||||
|
||||
$username = 'foo';
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->get($this->route . '/' . $account->id . '/external/')
|
||||
->assertStatus(404);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->json($this->method, $this->route . '/' . $account->id . '/external/', [
|
||||
'username' => $username,
|
||||
'domain' => 'bar',
|
||||
'password' => 'password',
|
||||
'protocol' => 'UDP'
|
||||
])->assertStatus(201);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->json($this->method, $this->route . '/' . $account->id . '/external/', [
|
||||
'username' => $username,
|
||||
'domain' => 'bar',
|
||||
'registrar' => 'bar',
|
||||
'password' => 'password',
|
||||
'protocol' => 'UDP'
|
||||
])->assertJsonValidationErrors(['registrar']);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->get($this->route . '/' . $account->id . '/external/')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->get($this->route . '/123/external/')
|
||||
->assertStatus(404);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->json($this->method, $this->route . '/' . $account->id . '/external/', [
|
||||
'username' => $username . '2',
|
||||
'domain' => 'bar',
|
||||
'protocol' => 'UDP'
|
||||
])->assertStatus(200);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->json($this->method, $this->route . '/' . $account->id . '/external/', [
|
||||
'username' => $username . '2',
|
||||
'domain' => 'bar',
|
||||
'realm' => 'newrealm',
|
||||
'protocol' => 'UDP'
|
||||
])->assertJsonValidationErrors(['password']);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->get($this->route . '/' . $account->id . '/external/')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $username . '2',
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->delete($this->route . '/' . $account->id . '/external')
|
||||
->assertStatus(200);
|
||||
|
||||
$this->keyAuthenticated($admin)
|
||||
->get($this->route . '/' . $account->id . '/external/')
|
||||
->assertStatus(404);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue