Fix FLEXIAPI-196 Add a phone validation system by country code with configuration panels and related tests and documentation

This commit is contained in:
Timothée Jaussoin 2024-07-17 11:56:34 +02:00
parent f6c5562201
commit 08ff1b8675
26 changed files with 933 additions and 71 deletions

View file

@ -3,6 +3,7 @@
v1.6
----
- Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app
- Fix FLEXIAPI-196 Add a phone validation system by country code with configuration panels and related tests and documentation
v1.5
---

View file

@ -42,7 +42,7 @@ function generateNonce(): string
function generateValidNonce(Account $account): string
{
$nonce = new DigestNonce;
$nonce = new DigestNonce();
$nonce->account_id = $account->id;
$nonce->nonce = generateNonce();
$nonce->save();
@ -62,7 +62,9 @@ function generatePin(): int
function percent($value, $max): float
{
if ($max == 0) $max = 1;
if ($max == 0) {
$max = 1;
}
return round(($value * 100) / $max, 2);
}
@ -81,8 +83,8 @@ function markdownDocumentationView(string $view): string
],
]);
$converter->getEnvironment()->addExtension(new HeadingPermalinkExtension);
$converter->getEnvironment()->addExtension(new TableOfContentsExtension);
$converter->getEnvironment()->addExtension(new HeadingPermalinkExtension());
$converter->getEnvironment()->addExtension(new TableOfContentsExtension());
return (string) $converter->convert(
(string)view($view, [
@ -162,3 +164,247 @@ function validateIsoDate($attribute, $value, $parameters, $validator): bool
return (bool)preg_match($regex, $value);
}
/**
* This list was got from the Internet
*
* @see https://gist.github.com/vxnick/380904
* @return array
*/
function getCountryCodes()
{
return [
'AF' => 'Afghanistan',
'AX' => 'Åland Islands',
'AL' => 'Albania',
'DZ' => 'Algeria',
'AS' => 'American Samoa',
'AD' => 'Andorra',
'AO' => 'Angola',
'AI' => 'Anguilla',
'AG' => 'Antigua & Barbuda',
'AR' => 'Argentina',
'AU' => 'Australia',
'AT' => 'Austria',
'AZ' => 'Azerbaijan',
'BS' => 'Bahamas',
'BH' => 'Bahrain',
'BD' => 'Bangladesh',
'BB' => 'Barbados',
'BY' => 'Belarus',
'BE' => 'Belgium',
'BZ' => 'Belize',
'BJ' => 'Benin',
'BM' => 'Bermuda',
'BT' => 'Bhutan',
'BO' => 'Bolivia',
'BA' => 'Bosnia & Herzegovina',
'BW' => 'Botswana',
'BR' => 'Brazil',
'IO' => 'British Indian Ocean Territory',
'BN' => 'Brunei',
'BG' => 'Bulgaria',
'BF' => 'Burkina Faso',
'BI' => 'Burundi',
'KH' => 'Cambodia',
'CM' => 'Cameroon',
'CA' => 'Canada',
'CV' => 'Cape Verde',
'KY' => 'Cayman Islands',
'CF' => 'Central African Republic',
'TD' => 'Chad',
'CL' => 'Chile',
'CN' => 'China',
'CX' => 'Christmas Island',
'CC' => 'Cocos (Keeling) Islands',
'CO' => 'Colombia',
'KM' => 'Comoros',
'CG' => 'Congo - Brazzaville',
'CD' => 'Congo - Kinshasa',
'CK' => 'Cook Islands',
'CR' => 'Costa Rica',
'CI' => 'Côte dIvoire',
'HR' => 'Croatia',
'CU' => 'Cuba',
'CY' => 'Cyprus',
'CZ' => 'Czechia',
'DK' => 'Denmark',
'DJ' => 'Djibouti',
'DM' => 'Dominica',
'DO' => 'Dominican Republic',
'EC' => 'Ecuador',
'EG' => 'Egypt',
'SV' => 'El Salvador',
'GQ' => 'Equatorial Guinea',
'ER' => 'Eritrea',
'EE' => 'Estonia',
'ET' => 'Ethiopia',
'FK' => 'Falkland Islands',
'FO' => 'Faroe Islands',
'FJ' => 'Fiji',
'FI' => 'Finland',
'FR' => 'France',
'GF' => 'French Guiana',
'PF' => 'French Polynesia',
'GA' => 'Gabon',
'GM' => 'Gambia',
'GE' => 'Georgia',
'DE' => 'Germany',
'GH' => 'Ghana',
'GI' => 'Gibraltar',
'GR' => 'Greece',
'GL' => 'Greenland',
'GD' => 'Grenada',
'GP' => 'Guadeloupe',
'GU' => 'Guam',
'GT' => 'Guatemala',
'GG' => 'Guernsey',
'GN' => 'Guinea',
'GW' => 'Guinea-Bissau',
'GY' => 'Guyana',
'HT' => 'Haiti',
'HN' => 'Honduras',
'HK' => 'Hong Kong SAR China',
'HU' => 'Hungary',
'IS' => 'Iceland',
'IN' => 'India',
'ID' => 'Indonesia',
'IR' => 'Iran',
'IQ' => 'Iraq',
'IE' => 'Ireland',
'IM' => 'Isle of Man',
'IL' => 'Israel',
'IT' => 'Italy',
'JM' => 'Jamaica',
'JP' => 'Japan',
'JE' => 'Jersey',
'JO' => 'Jordan',
'KZ' => 'Kazakhstan',
'KE' => 'Kenya',
'KI' => 'Kiribati',
'KP' => 'North Korea',
'KR' => 'South Korea',
'KW' => 'Kuwait',
'KG' => 'Kyrgyzstan',
'LA' => 'Laos',
'LV' => 'Latvia',
'LB' => 'Lebanon',
'LS' => 'Lesotho',
'LR' => 'Liberia',
'LY' => 'Libya',
'LI' => 'Liechtenstein',
'LT' => 'Lithuania',
'LU' => 'Luxembourg',
'MO' => 'Macao SAR China',
'MK' => 'North Macedonia',
'MG' => 'Madagascar',
'MW' => 'Malawi',
'MY' => 'Malaysia',
'MV' => 'Maldives',
'ML' => 'Mali',
'MT' => 'Malta',
'MH' => 'Marshall Islands',
'MQ' => 'Martinique',
'MR' => 'Mauritania',
'MU' => 'Mauritius',
'YT' => 'Mayotte',
'MX' => 'Mexico',
'FM' => 'Micronesia',
'MD' => 'Moldova',
'MC' => 'Monaco',
'MN' => 'Mongolia',
'ME' => 'Montenegro',
'MS' => 'Montserrat',
'MA' => 'Morocco',
'MZ' => 'Mozambique',
'MM' => 'Myanmar (Burma)',
'NA' => 'Namibia',
'NR' => 'Nauru',
'NP' => 'Nepal',
'NL' => 'Netherlands',
'NC' => 'New Caledonia',
'NZ' => 'New Zealand',
'NI' => 'Nicaragua',
'NE' => 'Niger',
'NG' => 'Nigeria',
'NU' => 'Niue',
'NF' => 'Norfolk Island',
'MP' => 'Northern Mariana Islands',
'NO' => 'Norway',
'OM' => 'Oman',
'PK' => 'Pakistan',
'PW' => 'Palau',
'PS' => 'Palestinian Territories',
'PA' => 'Panama',
'PG' => 'Papua New Guinea',
'PY' => 'Paraguay',
'PE' => 'Peru',
'PH' => 'Philippines',
'PL' => 'Poland',
'PT' => 'Portugal',
'PR' => 'Puerto Rico',
'QA' => 'Qatar',
'RE' => 'Réunion',
'RO' => 'Romania',
'RU' => 'Russia',
'RW' => 'Rwanda',
'SH' => 'St. Helena',
'KN' => 'St. Kitts & Nevis',
'LC' => 'St. Lucia',
'PM' => 'St. Pierre & Miquelon',
'VC' => 'St. Vincent & Grenadines',
'WS' => 'Samoa',
'SM' => 'San Marino',
'ST' => 'São Tomé & Príncipe',
'SA' => 'Saudi Arabia',
'SN' => 'Senegal',
'RS' => 'Serbia',
'SC' => 'Seychelles',
'SL' => 'Sierra Leone',
'SG' => 'Singapore',
'SK' => 'Slovakia',
'SI' => 'Slovenia',
'SB' => 'Solomon Islands',
'SO' => 'Somalia',
'ZA' => 'South Africa',
'ES' => 'Spain',
'LK' => 'Sri Lanka',
'SD' => 'Sudan',
'SR' => 'Suriname',
'SJ' => 'Svalbard & Jan Mayen',
'SZ' => 'Eswatini',
'SE' => 'Sweden',
'CH' => 'Switzerland',
'SY' => 'Syria',
'TW' => 'Taiwan',
'TJ' => 'Tajikistan',
'TZ' => 'Tanzania',
'TH' => 'Thailand',
'TL' => 'Timor-Leste',
'TG' => 'Togo',
'TK' => 'Tokelau',
'TO' => 'Tonga',
'TT' => 'Trinidad & Tobago',
'TN' => 'Tunisia',
'TM' => 'Turkmenistan',
'TC' => 'Turks & Caicos Islands',
'TV' => 'Tuvalu',
'UG' => 'Uganda',
'UA' => 'Ukraine',
'AE' => 'United Arab Emirates',
'GB' => 'United Kingdom',
'US' => 'United States',
'UY' => 'Uruguay',
'UZ' => 'Uzbekistan',
'VU' => 'Vanuatu',
'VE' => 'Venezuela',
'VN' => 'Vietnam',
'VG' => 'British Virgin Islands',
'VI' => 'U.S. Virgin Islands',
'WF' => 'Wallis & Futuna',
'EH' => 'Western Sahara',
'YE' => 'Yemen',
'ZM' => 'Zambia',
'ZW' => 'Zimbabwe',
];
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\PhoneCountry;
use Illuminate\Http\Request;
class PhoneCountryController extends Controller
{
public function index()
{
return view('admin.phone_country.index', [
'phone_countries' => PhoneCountry::all()
]);
}
public function activateAll()
{
PhoneCountry::query()->update(['activated' => true]);
return redirect()->route('admin.phone_countries.index');
}
public function deactivateAll()
{
PhoneCountry::query()->update(['activated' => false]);
return redirect()->route('admin.phone_countries.index');
}
public function activate(string $code)
{
$phoneCountry = PhoneCountry::where('code', $code)->firstOrFail();
PhoneCountry::where('country_code', $phoneCountry->country_code)->update(['activated' => true]);
return redirect()->route('admin.phone_countries.index');
}
public function deactivate(string $code)
{
$phoneCountry = PhoneCountry::where('code', $code)->firstOrFail();
PhoneCountry::where('country_code', $phoneCountry->country_code)->update(['activated' => false]);
return redirect()->route('admin.phone_countries.index');
}
}

View file

@ -38,9 +38,9 @@ use App\Mail\RegisterConfirmation;
use App\Rules\AccountCreationToken as RulesAccountCreationToken;
use App\Rules\AccountCreationTokenNotExpired;
use App\Rules\BlacklistedUsername;
use App\Rules\FilteredPhone;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\WithoutSpaces;
use App\Rules\PasswordAlgorithm;
use App\Services\AccountService;
@ -69,7 +69,7 @@ class AccountController extends Controller
$request->merge(['phone' => $phone]);
$request->validate([
'phone' => ['required', new WithoutSpaces, 'starts_with:+']
'phone' => ['required', 'phone', new FilteredPhone]
]);
$account = Account::where('domain', config('app.sip_domain'))
@ -116,9 +116,10 @@ class AccountController extends Controller
'phone' => [
'required_without:email',
'required_without:username',
'phone',
new FilteredPhone,
'unique:accounts,phone',
'unique:accounts,username',
new WithoutSpaces, 'starts_with:+'
],
'account_creation_token' => [
'required',
@ -189,7 +190,7 @@ class AccountController extends Controller
$request->validate([
'phone' => [
'required', new WithoutSpaces, 'starts_with:+', 'exists:accounts,phone'
'required', 'phone', new FilteredPhone, 'exists:accounts,phone'
],
'account_creation_token' => [
'required',

View file

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\PhoneCountry;
class PhoneCountryController extends Controller
{
public function index()
{
return PhoneCountry::all();
}
}

View file

@ -25,10 +25,10 @@ use Illuminate\Validation\Rule;
use App\Account;
use App\Rules\BlacklistedUsername;
use App\Rules\Dictionary;
use App\Rules\FilteredPhone;
use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\WithoutSpaces;
class Request extends FormRequest
{
@ -65,7 +65,8 @@ class Request extends FormRequest
'nullable',
'unique:accounts,phone',
'unique:accounts,username',
new WithoutSpaces(), 'starts_with:+'
'phone',
new FilteredPhone
]
];
}

View file

@ -24,10 +24,10 @@ use Illuminate\Validation\Rule;
use App\Account;
use App\Rules\BlacklistedUsername;
use App\Rules\FilteredPhone;
use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\WithoutSpaces;
class Request extends FormRequest
{
@ -60,11 +60,12 @@ class Request extends FormRequest
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'phone' => [
'nullable',
'phone',
new FilteredPhone,
Rule::unique('accounts', 'username')->where(function ($query) {
$query->where('domain', resolveDomain($this));
})->ignore($this->route('id'), 'id'),
Rule::unique('accounts', 'phone')->ignore($this->route('account_id'), 'id'),
new WithoutSpaces, 'starts_with:+'
]
];
}

View file

@ -0,0 +1,22 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PhoneCountry extends Model
{
use HasFactory;
public $incrementing = false;
protected $visible = ['code', 'country_code', 'activated'];
protected $casts = [
'activated' => 'boolean',
];
public function getNameAttribute(): ?string
{
return getCountryCodes()[$this->attributes['code']];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Rules;
use App\PhoneCountry;
use Illuminate\Contracts\Validation\Rule;
use Propaganistas\LaravelPhone\PhoneNumber;
class FilteredPhone implements Rule
{
public function passes($attribute, $value)
{
if (!PhoneCountry::where('code', (new PhoneNumber($value))->getCountry())
->where('activated', true)
->exists()) return false;
return true;
}
public function message()
{
return 'The phone number must belong to an authorized country.';
}
}

View file

@ -31,9 +31,9 @@ use App\Mail\NewsletterRegistration;
use App\Mail\RecoverByCode;
use App\Mail\RegisterValidation;
use App\PhoneChangeCode;
use App\Rules\FilteredPhone;
use Illuminate\Support\Facades\Log;
use App\Rules\WithoutSpaces;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
@ -223,9 +223,10 @@ class AccountService
{
$request->validate([
'phone' => [
'phone',
new FilteredPhone,
'required', 'unique:accounts,phone',
'unique:accounts,username',
new WithoutSpaces(), 'starts_with:+'
]
]);

View file

@ -20,6 +20,7 @@
"ovh/ovh": "^3.2",
"parsedown/laravel": "^1.2",
"phpunit/phpunit": "^9.6",
"propaganistas/laravel-phone": "^5.1",
"react/socket": "^1.14",
"respect/validation": "^2.2",
"sabre/vobject": "^4.5",

285
flexiapi/composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e9d79f33a899fd2dcde3de88c243e800",
"content-hash": "075490b395def258891e92f9518e7a03",
"packages": [
{
"name": "awobaz/compoships",
@ -298,16 +298,16 @@
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "f41715465d65213d644d3141a6a93081be5d3549"
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549",
"reference": "f41715465d65213d644d3141a6a93081be5d3549",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
@ -367,9 +367,9 @@
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2"
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2022-10-27T11:44:00+00:00"
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/cache",
@ -1389,6 +1389,135 @@
],
"time": "2023-10-12T05:21:21+00:00"
},
{
"name": "giggsey/libphonenumber-for-php",
"version": "8.13.40",
"source": {
"type": "git",
"url": "https://github.com/giggsey/libphonenumber-for-php.git",
"reference": "795e0b760e5c439b6fa1ffa787c1d90c2face1ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/795e0b760e5c439b6fa1ffa787c1d90c2face1ff",
"reference": "795e0b760e5c439b6fa1ffa787c1d90c2face1ff",
"shasum": ""
},
"require": {
"giggsey/locale": "^1.7|^2.0",
"php": ">=5.3.2",
"symfony/polyfill-mbstring": "^1.17"
},
"replace": {
"giggsey/libphonenumber-for-php-lite": "self.version"
},
"require-dev": {
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^1.0|^2.0",
"symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"libphonenumber\\": "src/"
},
"exclude-from-classmap": [
"/src/data/",
"/src/carrier/data/",
"/src/geocoding/data/",
"/src/timezone/data/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "PHP Port of Google's libphonenumber",
"homepage": "https://github.com/giggsey/libphonenumber-for-php",
"keywords": [
"geocoding",
"geolocation",
"libphonenumber",
"mobile",
"phonenumber",
"validation"
],
"support": {
"issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
"source": "https://github.com/giggsey/libphonenumber-for-php"
},
"time": "2024-07-01T11:38:07+00:00"
},
{
"name": "giggsey/locale",
"version": "2.6",
"source": {
"type": "git",
"url": "https://github.com/giggsey/Locale.git",
"reference": "37874fa473131247c348059fb7b8985efc18b5ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/Locale/zipball/37874fa473131247c348059fb7b8985efc18b5ea",
"reference": "37874fa473131247c348059fb7b8985efc18b5ea",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"ext-json": "*",
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^8.5|^9.5",
"symfony/console": "^5.0|^6.0",
"symfony/filesystem": "^5.0|^6.0",
"symfony/finder": "^5.0|^6.0",
"symfony/process": "^5.0|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Giggsey\\Locale\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "Locale functions required by libphonenumber-for-php",
"support": {
"issues": "https://github.com/giggsey/Locale/issues",
"source": "https://github.com/giggsey/Locale/tree/2.6"
},
"time": "2024-04-18T19:31:19+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.2",
@ -3177,16 +3306,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.0.2",
"version": "v5.1.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13"
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13",
"reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
"shasum": ""
},
"require": {
@ -3197,7 +3326,7 @@
},
"require-dev": {
"ircmaxell/php-yacc": "^0.0.7",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
"phpunit/phpunit": "^9.0"
},
"bin": [
"bin/php-parse"
@ -3229,9 +3358,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0"
},
"time": "2024-03-05T20:51:40+00:00"
"time": "2024-07-01T20:03:41+00:00"
},
{
"name": "nunomaduro/termwind",
@ -3945,45 +4074,45 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.19",
"version": "9.6.20",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
"reference": "49d7820565836236411f5dc002d16dd689cde42f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
"reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f",
"reference": "49d7820565836236411f5dc002d16dd689cde42f",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.3.1 || ^2",
"doctrine/instantiator": "^1.5.0 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.10.1",
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"myclabs/deep-copy": "^1.12.0",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=7.3",
"phpunit/php-code-coverage": "^9.2.28",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-code-coverage": "^9.2.31",
"phpunit/php-file-iterator": "^3.0.6",
"phpunit/php-invoker": "^3.1.1",
"phpunit/php-text-template": "^2.0.3",
"phpunit/php-timer": "^5.0.2",
"sebastian/cli-parser": "^1.0.1",
"sebastian/code-unit": "^1.0.6",
"phpunit/php-text-template": "^2.0.4",
"phpunit/php-timer": "^5.0.3",
"sebastian/cli-parser": "^1.0.2",
"sebastian/code-unit": "^1.0.8",
"sebastian/comparator": "^4.0.8",
"sebastian/diff": "^4.0.3",
"sebastian/environment": "^5.1.3",
"sebastian/exporter": "^4.0.5",
"sebastian/global-state": "^5.0.1",
"sebastian/object-enumerator": "^4.0.3",
"sebastian/resource-operations": "^3.0.3",
"sebastian/type": "^3.2",
"sebastian/diff": "^4.0.6",
"sebastian/environment": "^5.1.5",
"sebastian/exporter": "^4.0.6",
"sebastian/global-state": "^5.0.7",
"sebastian/object-enumerator": "^4.0.4",
"sebastian/resource-operations": "^3.0.4",
"sebastian/type": "^3.2.1",
"sebastian/version": "^3.0.2"
},
"suggest": {
@ -4028,7 +4157,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20"
},
"funding": [
{
@ -4044,7 +4173,79 @@
"type": "tidelift"
}
],
"time": "2024-04-05T04:35:58+00:00"
"time": "2024-07-10T11:45:39+00:00"
},
{
"name": "propaganistas/laravel-phone",
"version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/Propaganistas/Laravel-Phone.git",
"reference": "d8c0e5fbde9820e026595616a7a36074233a09c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Propaganistas/Laravel-Phone/zipball/d8c0e5fbde9820e026595616a7a36074233a09c6",
"reference": "d8c0e5fbde9820e026595616a7a36074233a09c6",
"shasum": ""
},
"require": {
"giggsey/libphonenumber-for-php": "^7.0|^8.0",
"illuminate/contracts": "^9.0|^10.0",
"illuminate/support": "^9.0|^10.0",
"illuminate/validation": "^9.0|^10.0",
"php": "^8.0"
},
"require-dev": {
"laravel/pint": "^1.4",
"nunomaduro/larastan": "^2.4",
"orchestra/testbench": "*",
"phpunit/phpunit": "^9.5.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Propaganistas\\LaravelPhone\\PhoneServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Propaganistas\\LaravelPhone\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Propaganistas",
"email": "Propaganistas@users.noreply.github.com"
}
],
"description": "Adds phone number functionality to Laravel based on Google's libphonenumber API.",
"keywords": [
"laravel",
"libphonenumber",
"phone",
"validation"
],
"support": {
"issues": "https://github.com/Propaganistas/Laravel-Phone/issues",
"source": "https://github.com/Propaganistas/Laravel-Phone/tree/5.1.1"
},
"funding": [
{
"url": "https://github.com/Propaganistas",
"type": "github"
}
],
"time": "2024-01-15T09:23:46+00:00"
},
{
"name": "psr/cache",
@ -5446,16 +5647,16 @@
},
{
"name": "sabre/vobject",
"version": "4.5.4",
"version": "4.5.5",
"source": {
"type": "git",
"url": "https://github.com/sabre-io/vobject.git",
"reference": "a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772"
"reference": "7148cf57d25aaba0a49f6656d37c35e8175b3087"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772",
"reference": "a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772",
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/7148cf57d25aaba0a49f6656d37c35e8175b3087",
"reference": "7148cf57d25aaba0a49f6656d37c35e8175b3087",
"shasum": ""
},
"require": {
@ -5546,7 +5747,7 @@
"issues": "https://github.com/sabre-io/vobject/issues",
"source": "https://github.com/fruux/sabre-vobject"
},
"time": "2023-11-09T12:54:37+00:00"
"time": "2024-07-02T08:48:52+00:00"
},
{
"name": "sabre/xml",

View file

@ -0,0 +1,70 @@
<?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 Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use libphonenumber\PhoneNumberUtil;
class PhoneCountryFactory extends Factory
{
public function definition()
{
$phoneNumberUtils = PhoneNumberUtil::getInstance();
$codes = ['AF', 'AX', 'AL', 'DZ', 'AS', 'AD'];
$code = $codes[array_rand($codes)];
return [
'code' => $code,
'country_code' => $phoneNumberUtils->getMetadataForRegion($code)->getCountryCode(),
'activated' => false,
];
}
public function france()
{
$code = 'FR';
$phoneNumberUtils = PhoneNumberUtil::getInstance();
return $this->state(fn (array $attributes) => [
'code' => $code,
'country_code' => $phoneNumberUtils->getMetadataForRegion($code)->getCountryCode()
]);
}
public function netherlands()
{
$code = 'NL';
$phoneNumberUtils = PhoneNumberUtil::getInstance();
return $this->state(fn (array $attributes) => [
'code' => $code,
'country_code' => $phoneNumberUtils->getMetadataForRegion($code)->getCountryCode()
]);
}
public function activated()
{
return $this->state(fn (array $attributes) => [
'activated' => true
]);
}
}

View file

@ -0,0 +1,34 @@
<?php
use App\PhoneCountry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use libphonenumber\PhoneNumberUtil;
return new class extends Migration
{
public function up()
{
Schema::create('phone_countries', function (Blueprint $table) {
$table->string('code', 2)->primary();
$table->string('country_code', 3);
$table->boolean('activated')->default(false);
$table->timestamps();
});
$phoneNumberUtils = PhoneNumberUtil::getInstance();
foreach (getCountryCodes() as $code => $name) {
$phoneCountry = new PhoneCountry();
$phoneCountry->code = $code;
$phoneCountry->country_code = $phoneNumberUtils->getMetadataForRegion($code)->getCountryCode();
$phoneCountry->save();
}
}
public function down()
{
Schema::dropIfExists('phone_countries');
}
};

View file

@ -432,6 +432,7 @@ content>nav a {
margin: 1rem 0;
position: relative;
white-space: nowrap;
padding: 0 1rem;
}
content>nav a.current {
@ -446,8 +447,7 @@ content>nav a.current i {
}
content>nav a i {
margin: 0 1rem;
margin-left: 2rem;
margin-right: 0.75rem;
font-size: 2rem;
}

View file

@ -0,0 +1,52 @@
@extends('layouts.main')
@section('breadcrumb')
<li class="breadcrumb-item" aria-current="page">
Phone Countries
</li>
@endsection
@section('content')
<header>
<h1><i class="material-symbols-outlined">flag</i> Phone Countries</h1>
<a class="btn btn-secondary oppose" href="{{ route('admin.phone_countries.activate_all') }}">
<i class="material-symbols-outlined">add_circle</i>
Activate All
</a>
<a class="btn btn-secondary" href="{{ route('admin.phone_countries.deactivate_all') }}">
<i class="material-symbols-outlined">remove_circle</i>
Deactivate All
</a>
</header>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Country code</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($phone_countries as $phone_country)
<tr>
<td>{{ $phone_country->code }}</td>
<td>{{ $phone_country->name }}</td>
<td>{{ $phone_country->country_code }}</td>
<td>
@if ($phone_country->activated)
<span class="badge badge-success" title="Activated">Activated</span>
<a href="{{ route('admin.phone_countries.deactivate', $phone_country->code) }}">Desactivate</a>
@else
<span class="badge badge-error" title="Deactivated">Deactivated</span>
<a href="{{ route('admin.phone_countries.activate', $phone_country->code) }}">Activate</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@endsection

View file

@ -243,7 +243,7 @@ JSON parameters:
* `algorithm` required, values can be `SHA-256` or `MD5`
* `domain` if not set the value is enforced to the default registration domain set in the global configuration
* `email` optional if `phone` set, an email, set an email to the account, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `phone` required if `username` not set, optional if `email` set, a phone number, set a phone number to the account
* `phone` required if `username` not set, optional if `email` set, a valid phone number, set a phone number to the account
* `account_creation_token` the unique `account_creation_token`
### `POST /accounts/with-account-creation-token`
@ -284,7 +284,7 @@ Can only be used once, a new `recover_key` need to be requested to be called aga
JSON parameters:
* `phone` required the phone number to send the SMS to
* `phone` required, the phone number to send the SMS to
* `account_creation_token` the unique `account_creation_token`
### `GET /accounts/{sip}/recover/{recover_key}`
@ -380,7 +380,7 @@ JSON parameters:
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default, create an admin account
* `phone` optional, a phone number, set a phone number to the account
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
* `dictionary` optional, an associative array attached to the account, <a href="#dictionary">see also the related endpoints</a>.
* <span class="badge badge-message">Deprecated</span> `confirmation_key_expires` optional, a datetime of this format: Y-m-d H:i:s. Only used when `activated` is not used or `false`. Enforces an expiration date on the returned `confirmation_key`. After that datetime public email or phone activation endpoints will return `403`.
@ -399,7 +399,7 @@ JSON parameters:
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default
* `phone` optional, a phone number, set a phone number to the account
* `phone` optional, a valid phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
Using this endpoint you can also set a fresh dictionnary if the parameter is set. The existing dictionary entries will be destroyed.
@ -765,6 +765,17 @@ JSON parameters:
* `to` required, SIP address of the receiver
* `body` required, content of the message
## Phone Countries
The phone numbers managed by FlexiAPI are validated against a list of countries that can be managed in the admin web panels.
### `GET /phones_countries`
<span class="badge badge-success">Public</span>
Return the list of Phone Countries and their current status.
If a country is deactivated all the new submitted phones submitted on the platform will be blocked.
## Statistics
FlexiAPI can record logs generated by the FlexiSIP server and compile them into statistics.

View file

@ -11,6 +11,7 @@
if (auth()->user()->superAdmin) {
$items['admin.sip_domains.index'] = ['title' => 'SIP Domains', 'icon' => 'dns'];
$items['admin.phone_countries.index'] = ['title' => 'Phone Countries', 'icon' => 'flag'];
}
}
@endphp

View file

@ -59,6 +59,8 @@ Route::post('accounts/auth_token', 'Api\Account\AuthTokenController@store');
Route::get('accounts/me/api_key/{auth_token}', 'Api\Account\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt');
Route::get('phone_countries', 'Api\PhoneCountryController@index');
Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blocked']], function () {
Route::get('accounts/auth_token/{auth_token}/attach', 'Api\Account\AuthTokenController@attach');
Route::post('account_creation_tokens/consume', 'Api\Account\CreationTokenController@consume');

View file

@ -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\PhoneCountryController;
use App\Http\Controllers\Admin\SipDomainController;
use App\Http\Controllers\Admin\StatisticsController;
use Illuminate\Support\Facades\Route;
@ -157,6 +158,14 @@ Route::group(['middleware' => 'web_panel_enabled'], function () {
Route::middleware(['auth.super_admin'])->group(function () {
Route::resource('sip_domains', SipDomainController::class);
Route::get('sip_domains/delete/{id}', 'Admin\SipDomainController@delete')->name('sip_domains.delete');
Route::name('phone_countries.')->controller(PhoneCountryController::class)->prefix('phone_countries')->group(function () {
Route::get('/', 'index')->name('index');
Route::get('/activate_all', 'activateAll')->name('activate_all');
Route::get('/deactivate_all', 'deactivateAll')->name('deactivate_all');
Route::get('/{code}/activate', 'activate')->name('activate');
Route::get('/{code}/deactivate', 'deactivate')->name('deactivate');
});
});
Route::name('statistics.')->controller(StatisticsController::class)->prefix('statistics')->group(function () {

View file

@ -36,7 +36,7 @@ class AccountBlockingTest extends TestCase
$this->keyAuthenticated($account)
->json($this->method, $this->route . '/me/phone/request', [
'phone' => '+331234'
'phone' => '+33612312312'
])->assertStatus(200);
$this->keyAuthenticated($account)

View file

@ -246,7 +246,7 @@ class ApiAccountCreationTokenTest extends TestCase
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me/phone/request', [
'phone' => '+33123'
'phone' => '+33612312312'
])
->assertStatus(200);
}

View file

@ -20,8 +20,8 @@
namespace Tests\Feature;
use App\Account;
use App\AccountCreationToken;
use App\PhoneChangeCode;
use App\PhoneCountry;
use Tests\TestCase;
class ApiAccountPhoneChangeTest extends TestCase
@ -55,7 +55,7 @@ class ApiAccountPhoneChangeTest extends TestCase
$this->keyAuthenticated($account)
->json($this->method, $this->route.'/request', [
'phone' => '+123123'
'phone' => '+33612312312'
])
->assertStatus(200);
@ -71,6 +71,35 @@ class ApiAccountPhoneChangeTest extends TestCase
->assertStatus(410);
}
public function testCreatePhoneByCountry()
{
$account = Account::factory()->withConsumedAccountCreationToken()->create();
$account->generateApiKey();
$frenchPhoneNumber = '+33612121212';
$dutchPhoneNumber = '+31612121212';
$this->keyAuthenticated($account)
->json($this->method, $this->route.'/request', [
'phone' => $frenchPhoneNumber
])
->assertStatus(200);
$this->keyAuthenticated($account)
->json($this->method, $this->route.'/request', [
'phone' => $dutchPhoneNumber
])
->assertJsonValidationErrors(['phone']);
PhoneCountry::where('code', 'NL')->update(['activated' => true]);
$this->keyAuthenticated($account)
->json($this->method, $this->route.'/request', [
'phone' => $dutchPhoneNumber
])
->assertStatus(200);
}
public function testUnvalidatedAccount()
{
$account = Account::factory()->create();

View file

@ -869,7 +869,7 @@ class ApiAccountTest extends TestCase
*/
public function testRecoverPhone()
{
$phone = '+3361234';
$phone = '+33612312312';
$password = Password::factory()->create();
$password->account->generateApiKey();
@ -892,7 +892,7 @@ class ApiAccountTest extends TestCase
// Wrong phone
$this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => '+331234', // wrong phone number
'phone' => '+33612312313', // wrong phone number
'account_creation_token' => $token->token
])->assertJsonValidationErrors(['phone']);
@ -923,7 +923,10 @@ class ApiAccountTest extends TestCase
]);
$this->get($this->route . '/+1234/info-by-phone')
->assertStatus(404);
->assertStatus(302);
$this->get($this->route . '/+33612312312/info-by-phone')
->assertStatus(200);
$this->json('GET', $this->route . '/' . $password->account->identifier . '/info-by-phone')
->assertJsonValidationErrors(['phone']);
@ -1030,7 +1033,7 @@ class ApiAccountTest extends TestCase
public function testCreatePublicPhone()
{
$phone = '+12345';
$phone = '+33612312312';
config()->set('app.dangerous_endpoints', true);

View file

@ -0,0 +1,79 @@
<?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 App\PhoneCountry;
use App\SipDomain;
use Tests\TestCase;
class ApiPhoneCountryTest extends TestCase
{
protected $route = '/api/phone_countries';
protected $method = 'POST';
protected $routeChangePhone = '/api/accounts/me/phone';
public function testCreatePhoneByCountry()
{
$account = Account::factory()->withConsumedAccountCreationToken()->create();
$account->generateApiKey();
$frenchPhoneNumber = '+33612121212';
$dutchPhoneNumber = '+31612121212';
$this->get($this->route)
->assertStatus(200)
->assertJsonFragment([
'code' => 'FR',
'activated' => true
])
->assertJsonFragment([
'code' => 'NL',
'activated' => false
]);
$this->keyAuthenticated($account)
->json($this->method, $this->routeChangePhone.'/request', [
'phone' => $frenchPhoneNumber
])
->assertStatus(200);
$this->keyAuthenticated($account)
->json($this->method, $this->routeChangePhone.'/request', [
'phone' => $dutchPhoneNumber
])
->assertJsonValidationErrors(['phone']);
PhoneCountry::where('code', 'NL')->update(['activated' => true]);
$this->get($this->route)
->assertStatus(200)
->assertJsonFragment([
'code' => 'NL',
'activated' => true
]);
$this->keyAuthenticated($account)
->json($this->method, $this->routeChangePhone.'/request', [
'phone' => $dutchPhoneNumber
])
->assertStatus(200);
}
}

View file

@ -21,7 +21,7 @@ namespace Tests;
use App\Password;
use App\Account;
use App\PhoneCountry;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -33,6 +33,15 @@ abstract class TestCase extends BaseTestCase
protected $route = '/api/accounts/me';
protected $method = 'GET';
public function setUp(): void
{
parent::setUp();
PhoneCountry::truncate();
PhoneCountry::factory()->france()->activated()->create();
PhoneCountry::factory()->netherlands()->create();
}
protected function disableBlockingService()
{
config()->set('app.blocking_amount_events_authorized_during_period', 0);