mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-01-18 10:28:07 +00:00
Compare commits
29 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2e93f77f | ||
|
|
2aef80c792 | ||
|
|
c67aabdf24 | ||
|
|
50ec273cdd | ||
|
|
96690590ae | ||
|
|
17300d3ae5 | ||
|
|
1f84042e59 | ||
|
|
3f3ddda282 | ||
|
|
7562218480 | ||
|
|
2ddad9d851 | ||
|
|
c102bcec77 | ||
|
|
25bec44486 | ||
|
|
6555112715 | ||
|
|
30b8e492d8 | ||
|
|
ec1bdba376 | ||
|
|
ca4320e734 | ||
|
|
1e5052da1d | ||
|
|
cf6b007f26 | ||
|
|
6ca20e6a9c | ||
|
|
d2b2a9dd6d | ||
|
|
82b5e967dc | ||
|
|
0d21f3fda9 | ||
|
|
9569c79008 | ||
|
|
e11d55e3f9 | ||
|
|
3f35954071 | ||
|
|
0a8eda05c4 | ||
|
|
c22e713e1a | ||
|
|
9cb5953209 | ||
|
|
233feef9d8 |
64 changed files with 1269 additions and 533 deletions
|
|
@ -14,6 +14,14 @@ debian11-deploy:
|
|||
- debian11-package
|
||||
- debian11-test
|
||||
|
||||
debian12-deploy:
|
||||
extends: .deploy
|
||||
script:
|
||||
- ./deploy_packages.sh debian bookworm
|
||||
needs:
|
||||
- debian12-package
|
||||
- debian12-test
|
||||
|
||||
remi-deploy:
|
||||
extends: .deploy
|
||||
script:
|
||||
|
|
@ -28,11 +36,12 @@ remi-deploy:
|
|||
stage: deploy
|
||||
tags: ["docker"]
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
- master
|
||||
- /^release/.*$/
|
||||
|
||||
before_script:
|
||||
- rm -f $CI_PROJECT_DIR/build/*devel*.rpm
|
||||
- rm -f $CI_PROJECT_DIR/build/*devel*.rpm # Remove devel packages
|
||||
- cd $CI_PROJECT_DIR/build/ && for file in *; do mv "$file" $(echo "$file" | sed -e 's/[^A-Za-z0-9._+-]//g'); done || true && cd .. # Rename non standard packages
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$DEPLOY_USER_KEY" | tr -d '\r' | ssh-add - > /dev/null
|
||||
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
|
|
|
|||
|
|
@ -4,12 +4,19 @@ rocky8-package:
|
|||
script:
|
||||
- make rpm
|
||||
|
||||
debian11-package:
|
||||
.debian_package:
|
||||
extends: .package
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
|
||||
script:
|
||||
- make deb
|
||||
|
||||
debian11-package:
|
||||
extends: .debian_package
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
|
||||
|
||||
debian12-package:
|
||||
extends: .debian_package
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION
|
||||
|
||||
remi-phpredis-package:
|
||||
extends: .remi-package
|
||||
before_script:
|
||||
|
|
@ -62,4 +69,4 @@ remi-xmlrpc-package:
|
|||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- flexiapi/vendor/
|
||||
- flexiapi/vendor/
|
||||
|
|
|
|||
|
|
@ -13,11 +13,8 @@ rocky8-test:
|
|||
- php artisan key:generate
|
||||
- vendor/bin/phpunit --log-junit $CI_PROJECT_DIR/flexiapi_phpunit.log
|
||||
|
||||
debian11-test:
|
||||
.debian-test:
|
||||
extends: .test
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
|
||||
needs:
|
||||
- debian11-package
|
||||
script:
|
||||
- pwd
|
||||
- apt update
|
||||
|
|
@ -29,6 +26,18 @@ debian11-test:
|
|||
- php artisan key:generate
|
||||
- vendor/bin/phpunit --log-junit $CI_PROJECT_DIR/flexiapi_phpunit.log
|
||||
|
||||
debian11-test:
|
||||
extends: .debian-test
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
|
||||
needs:
|
||||
- debian11-package
|
||||
|
||||
debian12-test:
|
||||
extends: .debian-test
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/debian12-php:$DEBIAN_12_IMAGE_VERSION
|
||||
needs:
|
||||
- debian12-package
|
||||
|
||||
remi-phpredis-test:
|
||||
extends: .test
|
||||
image: gitlab.linphone.org:4567/bc/public/docker/rocky8-php:$ROCKY_8_IMAGE_VERSION
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
variables:
|
||||
ROCKY_8_IMAGE_VERSION: 20230330_163028_remove_remi
|
||||
DEBIAN_11_IMAGE_VERSION: 20230322_172926_missing_tools
|
||||
DEBIAN_12_IMAGE_VERSION: 20230925_143235_enable_debian12_packaging
|
||||
PHP_REDIS_REMI_VERSION: php-pecl-redis5-5.3.6-1
|
||||
PHP_IGBINARY_REMI_VERSION: php-pecl-igbinary-3.2.14-1
|
||||
PHP_MSGPACK_REMI_VERSION: php-pecl-msgpack-2.1.2-1
|
||||
PHP_MSGPACK_REMI_VERSION: php-pecl-msgpack-2.2.0-1
|
||||
PHP_XMLRPC_REMI_VERSION: php-pecl-xmlrpc-1.0.0~rc3-2
|
||||
|
||||
include:
|
||||
|
|
@ -14,4 +15,4 @@ include:
|
|||
stages:
|
||||
- package
|
||||
- test
|
||||
- deploy
|
||||
- deploy
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,7 +1,12 @@
|
|||
# Flexisip Account Manager Changelog
|
||||
|
||||
v1.3.1
|
||||
------
|
||||
- Fix #111 Disable phone authentication routes when the related toggle is set to false
|
||||
|
||||
v1.3
|
||||
----
|
||||
- Fix #90 Deploy packages from release branches as well
|
||||
- Fix #58 Fix the packaging process to use git describe as a reference
|
||||
- Fix #58 Move the generated packages in the build directory, and fix the release and version format in the .spec
|
||||
- Fix #58 Refactor and cleanup the .gitlab-ci file
|
||||
|
|
@ -14,6 +19,11 @@ v1.3
|
|||
- Fix #79 Add a refresh_password parameter to the provisioning URLs
|
||||
- Fix #78 Add a APP_ACCOUNTS_EMAIL_UNIQUE environnement setting
|
||||
- Fix #30 Remove APP_EVERYONE_IS_ADMIN
|
||||
- Fix #97 Validate usernames with a configurable regex
|
||||
- Fix #95 PUT /accounts admin endpoint implementation
|
||||
- Fix #102 Implement AccountCreationRequestToken
|
||||
- Fix #92 Add two new endpoints regarding email account reset and account search per email for admins
|
||||
- Fix #94 Implement the deprecated endpoint changes + tests + documentation
|
||||
|
||||
v1.2
|
||||
----
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ APP_FLEXISIP_PUSHER_PATH=
|
|||
APP_FLEXISIP_PUSHER_FIREBASE_KEY=
|
||||
|
||||
APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the generated API Keys are valid
|
||||
APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two consecutive account_creation_token creation
|
||||
|
||||
# Risky toggles
|
||||
APP_ADMINS_MANAGE_MULTI_DOMAINS=false # Allow admins to handle all the accounts in the database
|
||||
|
|
@ -24,6 +25,7 @@ ACCOUNT_REALM=null # Default realm for the accounts, fallback to the domain if n
|
|||
ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts
|
||||
ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE=false
|
||||
ACCOUNT_BLACKLISTED_USERNAMES=
|
||||
ACCOUNT_USERNAME_REGEX="^[a-z0-9+_.-]*$"
|
||||
|
||||
# Account provisioning
|
||||
ACCOUNT_PROVISIONING_RC_FILE=
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ use App\Password;
|
|||
use App\EmailChanged;
|
||||
use App\Mail\ChangingEmail;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Account extends Authenticatable
|
||||
{
|
||||
|
|
@ -108,62 +109,62 @@ class Account extends Authenticatable
|
|||
|
||||
public function activationExpiration()
|
||||
{
|
||||
return $this->hasOne('App\ActivationExpiration');
|
||||
return $this->hasOne(ActivationExpiration::class);
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->hasOne('App\Admin');
|
||||
return $this->hasOne(Admin::class);
|
||||
}
|
||||
|
||||
public function alias()
|
||||
{
|
||||
return $this->hasOne('App\Alias');
|
||||
return $this->hasOne(Alias::class);
|
||||
}
|
||||
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->hasOne('App\ApiKey');
|
||||
return $this->hasOne(ApiKey::class);
|
||||
}
|
||||
|
||||
public function externalAccount()
|
||||
{
|
||||
return $this->hasOne('App\ExternalAccount');
|
||||
return $this->hasOne(ExternalAccount::class);
|
||||
}
|
||||
|
||||
public function contacts()
|
||||
{
|
||||
return $this->belongsToMany('App\Account', 'contacts', 'account_id', 'contact_id');
|
||||
return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id');
|
||||
}
|
||||
|
||||
public function emailChanged()
|
||||
{
|
||||
return $this->hasOne('App\EmailChanged');
|
||||
return $this->hasOne(EmailChanged::class);
|
||||
}
|
||||
|
||||
public function nonces()
|
||||
{
|
||||
return $this->hasMany('App\DigestNonce');
|
||||
return $this->hasMany(DigestNonce::class);
|
||||
}
|
||||
|
||||
public function authTokens()
|
||||
{
|
||||
return $this->hasMany('App\AuthToken');
|
||||
return $this->hasMany(AuthToken::class);
|
||||
}
|
||||
|
||||
public function passwords()
|
||||
{
|
||||
return $this->hasMany('App\Password');
|
||||
return $this->hasMany(Password::class);
|
||||
}
|
||||
|
||||
public function phoneChangeCode()
|
||||
{
|
||||
return $this->hasOne('App\PhoneChangeCode');
|
||||
return $this->hasOne(PhoneChangeCode::class);
|
||||
}
|
||||
|
||||
public function types()
|
||||
{
|
||||
return $this->belongsToMany('App\AccountType');
|
||||
return $this->belongsToMany(AccountType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -202,6 +203,19 @@ class Account extends Authenticatable
|
|||
return null;
|
||||
}
|
||||
|
||||
public function setPhoneAttribute(?string $phone)
|
||||
{
|
||||
$this->alias()->delete();
|
||||
|
||||
if (!empty($phone)) {
|
||||
$alias = new Alias;
|
||||
$alias->alias = $phone;
|
||||
$alias->domain = config('app.sip_domain');
|
||||
$alias->account_id = $this->id;
|
||||
$alias->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function getConfirmationKeyExpiresAttribute()
|
||||
{
|
||||
if ($this->activationExpiration) {
|
||||
|
|
@ -302,9 +316,20 @@ class Account extends Authenticatable
|
|||
return $this->provisioning_token;
|
||||
}
|
||||
|
||||
public function isAdmin()
|
||||
public function getAdminAttribute(): bool
|
||||
{
|
||||
return ($this->admin);
|
||||
return ($this->admin()->exists());
|
||||
}
|
||||
|
||||
public function setAdminAttribute(bool $isAdmin)
|
||||
{
|
||||
$this->admin()->delete();
|
||||
|
||||
if ($isAdmin) {
|
||||
$admin = new Admin;
|
||||
$admin->account_id = $this->id;
|
||||
$admin->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function hasTombstone()
|
||||
|
|
@ -325,6 +350,14 @@ class Account extends Authenticatable
|
|||
$password->save();
|
||||
}
|
||||
|
||||
public function fillPassword(Request $request)
|
||||
{
|
||||
if ($request->filled('password')) {
|
||||
$this->algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5';
|
||||
$this->updatePassword($request->get('password'), $this->algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public function toVcard4()
|
||||
{
|
||||
$vcard = 'BEGIN:VCARD
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ class AccountAction extends Model
|
|||
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Account');
|
||||
return $this->belongsTo(Account::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
flexiapi/app/AccountCreationRequestToken.php
Normal file
43
flexiapi/app/AccountCreationRequestToken.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;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AccountCreationRequestToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $hidden = ['id', 'updated_at', 'created_at'];
|
||||
protected $appends = ['validation_url'];
|
||||
|
||||
public function accountCreationToken()
|
||||
{
|
||||
return $this->belongsTo(AccountCreationToken::class, 'acc_creation_token_id');
|
||||
}
|
||||
|
||||
public function getValidationUrlAttribute(): ?string
|
||||
{
|
||||
return $this->validated_at == null
|
||||
? route('account.creation_request_token.check', $this->token)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,4 +25,11 @@ use Illuminate\Database\Eloquent\Model;
|
|||
class AccountCreationToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $hidden = ['id', 'updated_at', 'created_at'];
|
||||
|
||||
public function accountCreationRequestToken()
|
||||
{
|
||||
return $this->hasOne(AccountCreationRequestToken::class, 'acc_creation_token_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ class AccountType extends Model
|
|||
|
||||
public function accounts()
|
||||
{
|
||||
return $this->belongsToMany('App\Account');
|
||||
return $this->belongsToMany(Account::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ActivationExpiration extends Model
|
|||
|
||||
public function account()
|
||||
{
|
||||
return $this->belongsTo('App\Account');
|
||||
return $this->belongsTo(Account::class);
|
||||
}
|
||||
|
||||
public function isExpired()
|
||||
|
|
|
|||
|
|
@ -30,4 +30,16 @@ class Alias extends Model
|
|||
{
|
||||
return $this->belongsTo('App\Account');
|
||||
}
|
||||
|
||||
public function scopeSip($query, string $sip)
|
||||
{
|
||||
if (\str_contains($sip, '@')) {
|
||||
list($usernane, $domain) = explode('@', $sip);
|
||||
|
||||
return $query->where('alias', $usernane)
|
||||
->where('domain', $domain);
|
||||
};
|
||||
|
||||
return $query->where('id', '<', 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use Illuminate\Support\Str;
|
|||
use App\Account;
|
||||
use App\DigestNonce;
|
||||
use App\ExternalAccount;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
|
||||
|
|
@ -119,3 +120,13 @@ function isRegularExpression($string): bool
|
|||
|
||||
return $isRegularExpression;
|
||||
}
|
||||
|
||||
function resolveDomain(Request $request): string
|
||||
{
|
||||
return $request->has('domain')
|
||||
&& $request->user()
|
||||
&& $request->user()->admin
|
||||
&& config('app.admins_manage_multi_domains')
|
||||
? $request->get('domain')
|
||||
: config('app.sip_domain');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\AccountCreationRequestToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Rules\AccountCreationRequestToken as RulesAccountCreationRequestToken;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CreationRequestTokenController extends Controller
|
||||
{
|
||||
public function check(Request $request, string $creationRequestToken)
|
||||
{
|
||||
$request->merge(['account_creation_request_token' => $creationRequestToken]);
|
||||
$request->validate([
|
||||
'account_creation_request_token' => [
|
||||
'required',
|
||||
new RulesAccountCreationRequestToken
|
||||
]
|
||||
]);
|
||||
|
||||
$accountCreationRequestToken = AccountCreationRequestToken::where('token', $request->get('account_creation_request_token'))->firstOrFail();
|
||||
|
||||
return view('account.creation_request_token.check', [
|
||||
'account_creation_request_token' => $accountCreationRequestToken
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateToken(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'account_creation_request_token' => [
|
||||
'required',
|
||||
new RulesAccountCreationRequestToken
|
||||
],
|
||||
'g-recaptcha-response' => 'required|captcha',
|
||||
]);
|
||||
|
||||
$accountCreationRequestToken = AccountCreationRequestToken::where('token', $request->get('account_creation_request_token'))->firstOrFail();
|
||||
$accountCreationRequestToken->validated_at = Carbon::now();
|
||||
$accountCreationRequestToken->save();
|
||||
|
||||
return view('account.creation_request_token.valid');
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ class ProvisioningController extends Controller
|
|||
$params['reset_password'] = true;
|
||||
}
|
||||
|
||||
$url = route('provisioning.show', $params);
|
||||
$url = route('provisioning.provision', $params);
|
||||
|
||||
$result = Builder::create()
|
||||
->writer(new PngWriter())
|
||||
|
|
@ -73,7 +73,7 @@ class ProvisioningController extends Controller
|
|||
$account = $authToken->account;
|
||||
$authToken->delete();
|
||||
|
||||
return $this->show($request, null, $account);
|
||||
return $this->generateProvisioning($request, $account);
|
||||
}
|
||||
|
||||
abort(404);
|
||||
|
|
@ -84,10 +84,38 @@ class ProvisioningController extends Controller
|
|||
*/
|
||||
public function me(Request $request)
|
||||
{
|
||||
return $this->show($request, null, $request->user());
|
||||
return $this->generateProvisioning($request, $request->user());
|
||||
}
|
||||
|
||||
public function show(Request $request, $provisioningToken = null, Account $requestAccount = null)
|
||||
/**
|
||||
* Get the base provisioning, with authentication
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
return $this->generateProvisioning($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisioning Token based provisioning
|
||||
*/
|
||||
public function provision(Request $request, string $provisioningToken)
|
||||
{
|
||||
$account = Account::withoutGlobalScopes()
|
||||
->where('provisioning_token', $provisioningToken)
|
||||
->firstOrFail();
|
||||
|
||||
if ($account->activationExpired() || ($provisioningToken != $account->provisioning_token)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$account->activated = true;
|
||||
$account->provisioning_token = null;
|
||||
$account->save();
|
||||
|
||||
return $this->generateProvisioning($request, $account);
|
||||
}
|
||||
|
||||
private function generateProvisioning(Request $request, Account $account = null)
|
||||
{
|
||||
// Load the hooks if they exists
|
||||
$provisioningHooks = config_path('provisioning_hooks.php');
|
||||
|
|
@ -132,19 +160,8 @@ class ProvisioningController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$account = null;
|
||||
|
||||
// Account handling
|
||||
if ($requestAccount) {
|
||||
$account = $requestAccount;
|
||||
} elseif ($provisioningToken) {
|
||||
$account = Account::withoutGlobalScopes()
|
||||
->where('provisioning_token', $provisioningToken)
|
||||
->first();
|
||||
}
|
||||
|
||||
// Password reset
|
||||
if ($request->has('reset_password')) {
|
||||
if ($account && $request->has('reset_password')) {
|
||||
$account->updatePassword(Str::random(10));
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +174,7 @@ class ProvisioningController extends Controller
|
|||
|
||||
$config->appendChild($section);
|
||||
|
||||
if ($account && !$account->activationExpired()) {
|
||||
if ($account) {
|
||||
$externalAccount = $account->externalAccount;
|
||||
|
||||
$section = $dom->createElement('section');
|
||||
|
|
@ -224,18 +241,6 @@ class ProvisioningController extends Controller
|
|||
$authInfoIndex++;
|
||||
}
|
||||
|
||||
if ($provisioningToken) {
|
||||
// Activate the account
|
||||
if ($account->activated == false
|
||||
&& $provisioningToken == $account->provisioning_token
|
||||
) {
|
||||
$account->activated = true;
|
||||
}
|
||||
|
||||
$account->provisioning_token = null;
|
||||
$account->save();
|
||||
}
|
||||
|
||||
$proxyConfigIndex++;
|
||||
|
||||
// External Account handling
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Carbon\Carbon;
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ class RegisterController extends Controller
|
|||
|
||||
public function storeEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
$validator = Validator::make($request->all(), [
|
||||
'terms' => 'accepted',
|
||||
'privacy' => 'accepted',
|
||||
'username' => [
|
||||
|
|
@ -91,6 +92,15 @@ class RegisterController extends Controller
|
|||
: 'required|email|confirmed',
|
||||
]);
|
||||
|
||||
// Weird workaround to force the injections of the validation errors,
|
||||
// the redirection seems to clear them when the captcha is used
|
||||
if ($validator->fails()) {
|
||||
return view('account.register.email', [
|
||||
'errors' => $validator->messages(),
|
||||
'domain' => '@' . config('app.sip_domain')
|
||||
]);
|
||||
}
|
||||
|
||||
$account = new Account;
|
||||
$account->username = $request->get('username');
|
||||
$account->email = $request->get('email');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ namespace App\Http\Controllers\Admin;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
|
|
@ -31,13 +30,6 @@ use App\Alias;
|
|||
use App\ExternalAccount;
|
||||
use App\Http\Requests\CreateAccountRequest;
|
||||
use App\Http\Requests\UpdateAccountRequest;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use App\Rules\BlacklistedUsername;
|
||||
use App\Rules\IsNotPhoneNumber;
|
||||
use App\Rules\NoUppercase;
|
||||
use App\Rules\SIPUsername;
|
||||
use App\Rules\WithoutSpaces;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
|
@ -73,45 +65,19 @@ class AccountController extends Controller
|
|||
|
||||
public function store(CreateAccountRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => [
|
||||
'required',
|
||||
new NoUppercase,
|
||||
new IsNotPhoneNumber,
|
||||
new BlacklistedUsername,
|
||||
new SIPUsername,
|
||||
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
|
||||
$query->where('domain', $this->resolveDomain($request));
|
||||
}),
|
||||
'filled',
|
||||
],
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
config('app.account_email_unique') ? Rule::unique('accounts', 'email') : null
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'unique:aliases,alias',
|
||||
'unique:accounts,username',
|
||||
new WithoutSpaces, 'starts_with:+'
|
||||
]
|
||||
]);
|
||||
|
||||
$account = new Account;
|
||||
$account->username = $request->get('username');
|
||||
$account->email = $request->get('email');
|
||||
$account->display_name = $request->get('display_name');
|
||||
$account->domain = $this->resolveDomain($request);
|
||||
$account->domain = resolveDomain($request);
|
||||
$account->ip_address = $request->ip();
|
||||
$account->creation_time = Carbon::now();
|
||||
$account->user_agent = config('app.name');
|
||||
$account->dtmf_protocol = $request->get('dtmf_protocol');
|
||||
$account->save();
|
||||
|
||||
$this->fillPassword($request, $account);
|
||||
$this->fillPhone($request, $account);
|
||||
$account->phone = $request->get('phone');
|
||||
$account->fillPassword($request);
|
||||
|
||||
Log::channel('events')->info('Web Admin: Account created', ['id' => $account->identifier]);
|
||||
|
||||
|
|
@ -128,32 +94,6 @@ class AccountController extends Controller
|
|||
|
||||
public function update(UpdateAccountRequest $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => [
|
||||
'required',
|
||||
new NoUppercase,
|
||||
new IsNotPhoneNumber,
|
||||
new BlacklistedUsername,
|
||||
new SIPUsername,
|
||||
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
|
||||
$query->where('domain', $this->resolveDomain($request));
|
||||
})->ignore($id),
|
||||
'filled',
|
||||
],
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
config('app.account_email_unique') ? Rule::unique('accounts', 'email')->ignore($id) : null
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'unique:aliases,alias',
|
||||
'unique:accounts,username',
|
||||
new WithoutSpaces, 'starts_with:+'
|
||||
]
|
||||
]);
|
||||
|
||||
$account = Account::findOrFail($id);
|
||||
$account->username = $request->get('username');
|
||||
$account->email = $request->get('email');
|
||||
|
|
@ -161,8 +101,8 @@ class AccountController extends Controller
|
|||
$account->dtmf_protocol = $request->get('dtmf_protocol');
|
||||
$account->save();
|
||||
|
||||
$this->fillPassword($request, $account);
|
||||
$this->fillPhone($request, $account);
|
||||
$account->phone = $request->get('phone');
|
||||
$account->fillPassword($request);
|
||||
|
||||
Log::channel('events')->info('Web Admin: Account updated', ['id' => $account->identifier]);
|
||||
|
||||
|
|
@ -264,25 +204,4 @@ class AccountController extends Controller
|
|||
|
||||
return redirect()->route('admin.account.index');
|
||||
}
|
||||
|
||||
private function fillPassword(Request $request, Account $account)
|
||||
{
|
||||
if ($request->filled('password')) {
|
||||
$algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5';
|
||||
$account->updatePassword($request->get('password'), $algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private function fillPhone(Request $request, Account $account)
|
||||
{
|
||||
if ($request->filled('phone')) {
|
||||
$account->alias()->delete();
|
||||
|
||||
$alias = new Alias;
|
||||
$alias->alias = $request->get('phone');
|
||||
$alias->domain = config('app.sip_domain');
|
||||
$alias->account_id = $account->id;
|
||||
$alias->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
|
@ -28,12 +28,13 @@ use App\Http\Controllers\Controller;
|
|||
use Carbon\Carbon;
|
||||
|
||||
use App\Account;
|
||||
use App\AccountTombstone;
|
||||
use App\AccountCreationToken;
|
||||
use App\AccountTombstone;
|
||||
use App\Alias;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use App\Libraries\OvhSMS;
|
||||
use App\Mail\RegisterConfirmation;
|
||||
use App\Rules\AccountCreationToken as RulesAccountCreationToken;
|
||||
use App\Rules\BlacklistedUsername;
|
||||
use App\Rules\IsNotPhoneNumber;
|
||||
use App\Rules\NoUppercase;
|
||||
|
|
@ -70,11 +71,13 @@ class AccountController extends Controller
|
|||
$alias = Alias::where('alias', $phone)->first();
|
||||
$account = $alias
|
||||
? $alias->account
|
||||
: Account::sip($phone)->firstOrFail();
|
||||
// Injecting the default sip domain to try to resolve the account
|
||||
: Account::sip($phone . '@' . config('app.sip_domain'))->firstOrFail();
|
||||
|
||||
return \response()->json([
|
||||
'activated' => $account->activated,
|
||||
'realm' => $account->realm
|
||||
'realm' => $account->realm,
|
||||
'phone' => (bool)$alias
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +91,8 @@ class AccountController extends Controller
|
|||
|
||||
$request->validate([
|
||||
'username' => [
|
||||
'prohibits:phone',
|
||||
'required_without:phone',
|
||||
new NoUppercase,
|
||||
new IsNotPhoneNumber,
|
||||
new BlacklistedUsername,
|
||||
new SIPUsername,
|
||||
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
|
||||
|
|
@ -109,10 +111,14 @@ class AccountController extends Controller
|
|||
: 'required_without:phone|email',
|
||||
'phone' => [
|
||||
'required_without:email',
|
||||
'prohibits:username',
|
||||
'required_without:username',
|
||||
'unique:aliases,alias',
|
||||
'unique:accounts,username',
|
||||
new WithoutSpaces, 'starts_with:+'
|
||||
],
|
||||
'account_creation_token' => [
|
||||
'required',
|
||||
new RulesAccountCreationToken
|
||||
]
|
||||
]);
|
||||
|
||||
|
|
@ -127,12 +133,18 @@ class AccountController extends Controller
|
|||
: config('app.sip_domain');
|
||||
$account->ip_address = $request->ip();
|
||||
$account->creation_time = Carbon::now();
|
||||
$account->user_agent = config('app.name');
|
||||
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
|
||||
$account->provision();
|
||||
$account->save();
|
||||
|
||||
$account->updatePassword($request->get('password'), $request->get('algorithm'));
|
||||
|
||||
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
|
||||
$token->used = true;
|
||||
$token->account_id = $account->id;
|
||||
$token->save();
|
||||
|
||||
Log::channel('events')->info('API: AccountCreationToken redeemed', ['token' => $request->get('account_creation_token')]);
|
||||
Log::channel('events')->info('API: Account created using the public endpoint', ['id' => $account->identifier]);
|
||||
|
||||
// Send validation by phone
|
||||
|
|
@ -178,17 +190,27 @@ class AccountController extends Controller
|
|||
$request->validate([
|
||||
'phone' => [
|
||||
'required', new WithoutSpaces, 'starts_with:+'
|
||||
],
|
||||
'account_creation_token' => [
|
||||
'required',
|
||||
new RulesAccountCreationToken
|
||||
]
|
||||
]);
|
||||
|
||||
$alias = Alias::where('alias', $request->get('phone'))->first();
|
||||
$account = $alias
|
||||
? $alias->account
|
||||
: Account::sip($request->get('phone'))->firstOrFail();
|
||||
: Account::sip($request->get('phone') . '@' . config('app.sip_domain'))->firstOrFail();
|
||||
|
||||
$account->confirmation_key = generatePin();
|
||||
$account->save();
|
||||
|
||||
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
|
||||
$token->used = true;
|
||||
$token->account_id = $account->id;
|
||||
$token->save();
|
||||
|
||||
Log::channel('events')->info('API: AccountCreationToken redeemed', ['token' => $request->get('account_creation_token')]);
|
||||
Log::channel('events')->info('API: Account recovery by phone', ['id' => $account->identifier]);
|
||||
|
||||
$ovhSMS = new OvhSMS;
|
||||
|
|
@ -202,9 +224,12 @@ class AccountController extends Controller
|
|||
{
|
||||
if (!config('app.dangerous_endpoints')) return abort(404);
|
||||
|
||||
$account = Account::sip($sip)
|
||||
->where('confirmation_key', $recoveryKey)
|
||||
->firstOrFail();
|
||||
$alias = Alias::sip($sip)->first();
|
||||
$account = $alias
|
||||
? $alias->account
|
||||
: Account::sip($sip)->firstOrFail();
|
||||
|
||||
if ($account->confirmation_key != $recoveryKey) abort(404);
|
||||
|
||||
if ($account->activationExpired()) abort(403, 'Activation expired');
|
||||
|
||||
|
|
@ -240,29 +265,14 @@ class AccountController extends Controller
|
|||
'password' => 'required|filled',
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'account_creation_token' => [
|
||||
'required_without:token',
|
||||
Rule::exists('account_creation_tokens', 'token')->where(function ($query) {
|
||||
$query->where('used', false);
|
||||
}),
|
||||
'size:' . WebAuthenticateController::$emailCodeSize
|
||||
'required',
|
||||
new RulesAccountCreationToken
|
||||
],
|
||||
'email' => config('app.account_email_unique')
|
||||
? 'nullable|email|unique:accounts,email'
|
||||
: 'nullable|email',
|
||||
// For retro-compatibility
|
||||
'token' => [
|
||||
'required_without:account_creation_token',
|
||||
Rule::exists('account_creation_tokens', 'token')->where(function ($query) {
|
||||
$query->where('used', false);
|
||||
}),
|
||||
'size:' . WebAuthenticateController::$emailCodeSize
|
||||
],
|
||||
]);
|
||||
|
||||
$token = AccountCreationToken::where('token', $request->get('token') ?? $request->get('account_creation_token'))->first();
|
||||
$token->used = true;
|
||||
$token->save();
|
||||
|
||||
$account = new Account;
|
||||
$account->username = $request->get('username');
|
||||
$account->email = $request->get('email');
|
||||
|
|
@ -277,6 +287,11 @@ class AccountController extends Controller
|
|||
|
||||
$account->updatePassword($request->get('password'), $request->get('algorithm'));
|
||||
|
||||
$token = AccountCreationToken::where('token', $request->get('account_creation_token'))->first();
|
||||
$token->used = true;
|
||||
$token->account_id = $account->id;
|
||||
$token->save();
|
||||
|
||||
Log::channel('events')->info('API: Account created', ['id' => $account->identifier]);
|
||||
|
||||
// Full reload
|
||||
|
|
@ -285,12 +300,17 @@ class AccountController extends Controller
|
|||
|
||||
public function activateEmail(Request $request, string $sip)
|
||||
{
|
||||
// For retro-compatibility
|
||||
if ($request->has('code')) {
|
||||
$request->merge(['confirmation_key' => $request->get('code')]);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'code' => 'required|size:' . WebAuthenticateController::$emailCodeSize
|
||||
'confirmation_key' => 'required|size:' . WebAuthenticateController::$emailCodeSize
|
||||
]);
|
||||
|
||||
$account = Account::sip($sip)
|
||||
->where('confirmation_key', $request->get('code'))
|
||||
->where('confirmation_key', $request->get('confirmation_key'))
|
||||
->firstOrFail();
|
||||
|
||||
if ($account->activationExpired()) abort(403, 'Activation expired');
|
||||
|
|
@ -306,12 +326,17 @@ class AccountController extends Controller
|
|||
|
||||
public function activatePhone(Request $request, string $sip)
|
||||
{
|
||||
// For retro-compatibility
|
||||
if ($request->has('code')) {
|
||||
$request->merge(['confirmation_key' => $request->get('code')]);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'code' => 'required|digits:4'
|
||||
'confirmation_key' => 'required|digits:4'
|
||||
]);
|
||||
|
||||
$account = Account::sip($sip)
|
||||
->where('confirmation_key', $request->get('code'))
|
||||
->where('confirmation_key', $request->get('confirmation_key'))
|
||||
->firstOrFail();
|
||||
|
||||
if ($account->activationExpired()) abort(403, 'Activation expired');
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\AuthToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\AuthToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class AccountContactController extends Controller
|
||||
class ContactController extends Controller
|
||||
{
|
||||
private $selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol'];
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\AccountCreationRequestToken;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class CreationRequestToken extends Controller
|
||||
{
|
||||
public function create(Request $request)
|
||||
{
|
||||
$creationRequestToken = new AccountCreationRequestToken;
|
||||
$creationRequestToken->token = Str::random(WebAuthenticateController::$emailCodeSize);
|
||||
$creationRequestToken->save();
|
||||
|
||||
return $creationRequestToken;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,18 +17,21 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
use App\AccountCreationToken;
|
||||
use App\Libraries\FlexisipPusherConnector;
|
||||
use App\AccountCreationRequestToken;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use App\Libraries\FlexisipPusherConnector;
|
||||
use App\Rules\AccountCreationRequestToken as RulesAccountCreationRequestToken;
|
||||
|
||||
class AccountCreationTokenController extends Controller
|
||||
class CreationTokenController extends Controller
|
||||
{
|
||||
public function sendByPush(Request $request)
|
||||
{
|
||||
|
|
@ -38,6 +41,19 @@ class AccountCreationTokenController extends Controller
|
|||
'pn_prid' => 'required',
|
||||
]);
|
||||
|
||||
$last = AccountCreationToken::where('pn_provider', $request->get('pn_provider'))
|
||||
->where('pn_param', $request->get('pn_param'))
|
||||
->where('pn_prid', $request->get('pn_prid'))
|
||||
->where('created_at', '>=', Carbon::now()->subMinutes(config('app.account_creation_token_retry_minutes'))->toDateTimeString())
|
||||
->where('used', true)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($last) {
|
||||
Log::channel('events')->info('API: Token throttled', ['token' => $last->token]);
|
||||
abort(429, 'Last token requested too recently');
|
||||
}
|
||||
|
||||
$token = new AccountCreationToken;
|
||||
$token->token = Str::random(WebAuthenticateController::$emailCodeSize);
|
||||
$token->pn_provider = $request->get('pn_provider');
|
||||
|
|
@ -55,4 +71,32 @@ class AccountCreationTokenController extends Controller
|
|||
|
||||
abort(503, "Token not sent");
|
||||
}
|
||||
|
||||
public function usingAccountRequestToken(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'account_creation_request_token' => [
|
||||
'required',
|
||||
new RulesAccountCreationRequestToken
|
||||
]
|
||||
]);
|
||||
|
||||
$creationRequestToken = AccountCreationRequestToken::where('token', $request->get('account_creation_request_token'))
|
||||
->where('used', false)
|
||||
->first();
|
||||
|
||||
if ($creationRequestToken && $creationRequestToken->validated_at != null) {
|
||||
$accountCreationToken = new AccountCreationToken;
|
||||
$accountCreationToken->token = Str::random(WebAuthenticateController::$emailCodeSize);
|
||||
$accountCreationToken->save();
|
||||
|
||||
$creationRequestToken->used = true;
|
||||
$creationRequestToken->acc_creation_token_id = $accountCreationToken->id;
|
||||
$creationRequestToken->save();
|
||||
|
||||
return $accountCreationToken;
|
||||
}
|
||||
|
||||
return abort(404);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Account;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -29,7 +29,7 @@ use App\Libraries\OvhSMS;
|
|||
use App\PhoneChangeCode;
|
||||
use App\Alias;
|
||||
|
||||
class AccountPhoneController extends Controller
|
||||
class PhoneController extends Controller
|
||||
{
|
||||
public function requestUpdate(Request $request)
|
||||
{
|
||||
|
|
@ -22,7 +22,6 @@ namespace App\Http\Controllers\Api\Admin;
|
|||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
|
|
@ -33,11 +32,10 @@ use App\ActivationExpiration;
|
|||
use App\Admin;
|
||||
use App\Alias;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use App\Rules\BlacklistedUsername;
|
||||
use App\Rules\IsNotPhoneNumber;
|
||||
use App\Rules\NoUppercase;
|
||||
use App\Rules\SIPUsername;
|
||||
use App\Rules\WithoutSpaces;
|
||||
use App\Http\Requests\CreateAccountRequest;
|
||||
use App\Http\Requests\UpdateAccountRequest;
|
||||
use App\Mail\PasswordAuthentication;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
|
|
@ -56,6 +54,11 @@ class AccountController extends Controller
|
|||
return Account::sip($sip)->firstOrFail();
|
||||
}
|
||||
|
||||
public function searchByEmail(Request $request, string $email)
|
||||
{
|
||||
return Account::where('email', $email)->firstOrFail();
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$account = Account::findOrFail($id);
|
||||
|
|
@ -105,36 +108,15 @@ class AccountController extends Controller
|
|||
return $account->makeVisible(['provisioning_token']);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(CreateAccountRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => [
|
||||
'required',
|
||||
new NoUppercase,
|
||||
new IsNotPhoneNumber,
|
||||
new BlacklistedUsername,
|
||||
new SIPUsername,
|
||||
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
|
||||
$query->where('domain', $this->resolveDomain($request));
|
||||
}),
|
||||
'filled',
|
||||
],
|
||||
'algorithm' => 'required|in:SHA-256,MD5',
|
||||
'password' => 'required|filled',
|
||||
'admin' => 'boolean|nullable',
|
||||
'activated' => 'boolean|nullable',
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'confirmation_key_expires' => [
|
||||
'date_format:Y-m-d H:i:s',
|
||||
'nullable',
|
||||
],
|
||||
'email' => config('app.account_email_unique')
|
||||
? 'nullable|email|unique:accounts,email'
|
||||
: 'nullable|email',
|
||||
'phone' => [
|
||||
'unique:aliases,alias',
|
||||
'unique:accounts,username',
|
||||
new WithoutSpaces, 'starts_with:+'
|
||||
]
|
||||
]);
|
||||
|
||||
|
|
@ -146,7 +128,7 @@ class AccountController extends Controller
|
|||
$account->ip_address = $request->ip();
|
||||
$account->dtmf_protocol = $request->get('dtmf_protocol');
|
||||
$account->creation_time = Carbon::now();
|
||||
$account->domain = $this->resolveDomain($request);
|
||||
$account->domain = resolveDomain($request);
|
||||
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
|
||||
|
||||
if (!$request->has('activated') || !(bool)$request->get('activated')) {
|
||||
|
|
@ -165,27 +147,45 @@ class AccountController extends Controller
|
|||
}
|
||||
|
||||
$account->updatePassword($request->get('password'), $request->get('algorithm'));
|
||||
|
||||
if ($request->has('admin') && (bool)$request->get('admin')) {
|
||||
$admin = new Admin;
|
||||
$admin->account_id = $account->id;
|
||||
$admin->save();
|
||||
}
|
||||
|
||||
if ($request->has('phone')) {
|
||||
$alias = new Alias;
|
||||
$alias->alias = $request->get('phone');
|
||||
$alias->domain = config('app.sip_domain');
|
||||
$alias->account_id = $account->id;
|
||||
$alias->save();
|
||||
}
|
||||
$account->admin = $request->has('admin') && (bool)$request->get('admin');
|
||||
$account->phone = $request->get('phone');
|
||||
|
||||
// Full reload
|
||||
$account = Account::withoutGlobalScopes()->find($account->id);
|
||||
|
||||
Log::channel('events')->info('API: Admin: Account created', ['id' => $account->identifier]);
|
||||
Log::channel('events')->info('API Admin: Account created', ['id' => $account->identifier]);
|
||||
|
||||
return response()->json($account->makeVisible(['confirmation_key', 'provisioning_token']));
|
||||
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
|
||||
}
|
||||
|
||||
public function update(UpdateAccountRequest $request, int $accountId)
|
||||
{
|
||||
$request->validate([
|
||||
'algorithm' => 'required|in:SHA-256,MD5',
|
||||
'admin' => 'boolean|nullable',
|
||||
'activated' => 'boolean|nullable'
|
||||
]);
|
||||
|
||||
$account = Account::findOrFail($accountId);
|
||||
$account->username = $request->get('username');
|
||||
$account->email = $request->get('email');
|
||||
$account->display_name = $request->get('display_name');
|
||||
$account->dtmf_protocol = $request->get('dtmf_protocol');
|
||||
$account->domain = resolveDomain($request);
|
||||
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
|
||||
|
||||
$account->save();
|
||||
|
||||
$account->updatePassword($request->get('password'), $request->get('algorithm'));
|
||||
$account->admin = $request->has('admin') && (bool)$request->get('admin');
|
||||
$account->phone = $request->get('phone');
|
||||
|
||||
// Full reload
|
||||
$account = Account::withoutGlobalScopes()->find($account->id);
|
||||
|
||||
Log::channel('events')->info('API Admin: Account updated', ['id' => $account->identifier]);
|
||||
|
||||
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
|
||||
}
|
||||
|
||||
public function typeAdd(int $id, int $typeId)
|
||||
|
|
@ -207,4 +207,18 @@ class AccountController extends Controller
|
|||
|
||||
return Account::findOrFail($id)->types()->detach($typeId);
|
||||
}
|
||||
|
||||
public function recoverByEmail(int $id)
|
||||
{
|
||||
$account = Account::findOrFail($id);
|
||||
$account->provision();
|
||||
$account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize);
|
||||
$account->save();
|
||||
|
||||
Log::channel('events')->info('API Admin: Sending recovery email', ['id' => $account->identifier]);
|
||||
|
||||
Mail::to($account)->send(new PasswordAuthentication($account));
|
||||
|
||||
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class AccountTypeController extends Controller
|
|||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'key' => ['required', 'alpha_dash', new NoUppercase],
|
||||
'key' => ['required', 'alpha_dash', new NoUppercase, 'unique:account_types,key'],
|
||||
]);
|
||||
|
||||
$accountType = new AccountType;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
|
|
@ -11,14 +11,4 @@ use Illuminate\Routing\Controller as BaseController;
|
|||
class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||
|
||||
protected function resolveDomain(Request $request): string
|
||||
{
|
||||
return $request->has('domain')
|
||||
&& $request->user()
|
||||
&& $request->user()->isAdmin()
|
||||
&& config('app.admins_manage_multi_domains')
|
||||
? $request->get('domain')
|
||||
: config('app.sip_domain');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class AuthenticateAdmin
|
|||
return redirect()->route('account.login');
|
||||
}
|
||||
|
||||
if (!$request->user()->isAdmin()) {
|
||||
if (!$request->user()->admin) {
|
||||
return abort(403, 'Unauthorized area');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,13 +29,14 @@ class CreateAccountRequest extends FormRequest
|
|||
new BlacklistedUsername,
|
||||
new SIPUsername,
|
||||
Rule::unique('accounts', 'username')->where(function ($query) {
|
||||
$query->where('domain', config('app.sip_domain'));
|
||||
$query->where('domain', resolveDomain($this));
|
||||
}),
|
||||
'filled',
|
||||
],
|
||||
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '',
|
||||
'password' => 'required|min:3',
|
||||
'email' => 'nullable|email',
|
||||
'email' => config('app.account_email_unique')
|
||||
? 'nullable|email|unique:accounts,email'
|
||||
: 'nullable|email',
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'phone' => [
|
||||
'nullable',
|
||||
|
|
|
|||
|
|
@ -33,8 +33,11 @@ class UpdateAccountRequest extends FormRequest
|
|||
})->ignore($this->route('id'), 'id'),
|
||||
'filled',
|
||||
],
|
||||
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '',
|
||||
'email' => 'nullable|email',
|
||||
'email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
config('app.account_email_unique') ? Rule::unique('accounts', 'email')->ignore($this->route('id')) : null
|
||||
],
|
||||
'password_sha256' => 'nullable|min:3',
|
||||
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
|
||||
'phone' => [
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class OvhSMS
|
|||
);
|
||||
|
||||
try {
|
||||
$smsServices = $this->api->get('/sms/');
|
||||
$smsServices = $this->api->get('/sms');
|
||||
|
||||
if (!empty($smsServices)) {
|
||||
$this->smsService = $smsServices[0];
|
||||
|
|
@ -71,6 +71,8 @@ class OvhSMS
|
|||
'validityPeriod' => 2880
|
||||
];
|
||||
|
||||
Log::channel('events')->info('OVH SMS sending', ['to' => $to, 'message' => $message]);
|
||||
|
||||
try {
|
||||
$this->api->post('/sms/'. $this->smsService . '/jobs', $content);
|
||||
// One credit removed
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class PasswordAuthentication extends Mailable
|
|||
: 'mails.authentication_text')
|
||||
->with([
|
||||
'link' => route('account.authenticate.email_confirm', [$this->account->confirmation_key]),
|
||||
'provisioning_link' => route('provisioning.show', [
|
||||
'provisioning_link' => route('provisioning.provision', [
|
||||
'provisioning_token' => $this->account->provisioning_token,
|
||||
'reset_password' => true
|
||||
]),
|
||||
|
|
|
|||
38
flexiapi/app/Rules/AccountCreationRequestToken.php
Normal file
38
flexiapi/app/Rules/AccountCreationRequestToken.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?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\Rules;
|
||||
|
||||
use App\AccountCreationRequestToken as AppAccountCreationRequestToken;
|
||||
use App\Http\Controllers\Account\AuthenticateController;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class AccountCreationRequestToken implements Rule
|
||||
{
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
return AppAccountCreationRequestToken::where('token', $value)->where('used', false)->exists()
|
||||
&& strlen($value) == AuthenticateController::$emailCodeSize;
|
||||
}
|
||||
|
||||
public function message()
|
||||
{
|
||||
return 'Please provide a valid account_creation_request_token';
|
||||
}
|
||||
}
|
||||
38
flexiapi/app/Rules/AccountCreationToken.php
Normal file
38
flexiapi/app/Rules/AccountCreationToken.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?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\Rules;
|
||||
|
||||
use App\AccountCreationToken as AppAccountCreationToken;
|
||||
use App\Http\Controllers\Account\AuthenticateController;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class AccountCreationToken implements Rule
|
||||
{
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
return AppAccountCreationToken::where('token', $value)->where('used', false)->exists()
|
||||
&& strlen($value) == AuthenticateController::$emailCodeSize;
|
||||
}
|
||||
|
||||
public function message()
|
||||
{
|
||||
return 'Please provide a valid account_creation_token';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,21 @@
|
|||
<?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\Rules;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SIPUsername implements Rule
|
|||
{
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
return Validator::regex('/^[a-z0-9+_.-]*$/')->validate($value);
|
||||
return Validator::regex('/' . config('app.account_username_regex') . '/')->validate($value);
|
||||
}
|
||||
|
||||
public function message()
|
||||
|
|
|
|||
|
|
@ -31,12 +31,18 @@ return [
|
|||
'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false),
|
||||
'consume_external_account_on_create' => env('ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE', false),
|
||||
'blacklisted_usernames' => env('ACCOUNT_BLACKLISTED_USERNAMES', ''),
|
||||
'account_username_regex' => env('ACCOUNT_USERNAME_REGEX', '^[a-z0-9+_.-]*$'),
|
||||
|
||||
/**
|
||||
* Time limit before the API Key and related cookie are expired
|
||||
*/
|
||||
'api_key_expiration_minutes' => env('APP_API_KEY_EXPIRATION_MINUTES', 60),
|
||||
|
||||
/**
|
||||
* Amount of minutes before re-authorizing the generation of a new account creation token
|
||||
*/
|
||||
'account_creation_token_retry_minutes' => env('APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES', 60),
|
||||
|
||||
/**
|
||||
* External interfaces
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ use Illuminate\Support\Str;
|
|||
|
||||
use App\AccountCreationToken;
|
||||
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class AccountCreationTokenFactory extends Factory
|
||||
{
|
||||
|
|
@ -36,7 +37,8 @@ class AccountCreationTokenFactory extends Factory
|
|||
'pn_param' => $this->faker->uuid,
|
||||
'pn_prid' => $this->faker->uuid,
|
||||
'token' => Str::random(WebAuthenticateController::$emailCodeSize),
|
||||
'used' => false
|
||||
'used' => false,
|
||||
'created_at' => Carbon::now()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddEmailIndexToAccountsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('accounts', function (Blueprint $table) {
|
||||
$table->index('email');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('accounts', function (Blueprint $table) {
|
||||
$table->dropIndex('accounts_email_index');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use App\AccountCreationToken;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MakePnAttributesNullableAccountCreationTokensTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('account_creation_tokens', function (Blueprint $table) {
|
||||
$table->string('pn_provider')->nullable(true)->change();
|
||||
$table->string('pn_param')->nullable(true)->change();
|
||||
$table->string('pn_prid')->nullable(true)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
AccountCreationToken::whereNull('pn_provider')->delete();
|
||||
|
||||
Schema::table('account_creation_tokens', function (Blueprint $table) {
|
||||
$table->string('pn_provider')->nullable(false)->change();
|
||||
$table->string('pn_param')->nullable(false)->change();
|
||||
$table->string('pn_prid')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAccountCreationRequestTokensTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('account_creation_request_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('token', 16)->index();
|
||||
$table->boolean('used')->default(false);
|
||||
$table->dateTime('validated_at')->nullable();
|
||||
|
||||
$table->bigInteger('acc_creation_token_id')->unsigned()->nullable();
|
||||
$table->foreign('acc_creation_token_id')->references('id')
|
||||
->on('account_creation_tokens')->onDelete('cascade');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('account_creation_request_tokens');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Handle Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
|
||||
RewriteBase /flexiapi/
|
||||
</IfModule>
|
||||
4
flexiapi/public/css/style.css
vendored
4
flexiapi/public/css/style.css
vendored
|
|
@ -29,6 +29,10 @@ body > div {
|
|||
max-width: 800px;
|
||||
}
|
||||
|
||||
.container.large {
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
body > footer::before {
|
||||
background-color: white;
|
||||
background-position: bottom center;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
{!! Form::open(['route' => 'account.creation_request_token.validate']) !!}
|
||||
{!! Form::hidden('account_creation_request_token', $account_creation_request_token->token) !!}
|
||||
@include('parts.captcha')
|
||||
{!! Form::submit('I\'m not a robot', ['class' => 'btn btn-primary btn-centered']) !!}
|
||||
{!! Form::close() !!}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@extends('layouts.main')
|
||||
|
||||
@section('content')
|
||||
<h3 class="text-center mt-5">Thanks for the validation</h3>
|
||||
<p class="text-center">You can now continue your registration process in the application</p>
|
||||
@endsection
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
@if($account->isAdmin())
|
||||
@if($account->admin)
|
||||
<h3>Admin area</h3>
|
||||
<div class="list-group mb-3">
|
||||
<a href="{{ route('admin.account.index') }}" class="list-group-item list-group-item-action">
|
||||
|
|
|
|||
|
|
@ -95,4 +95,4 @@
|
|||
{!! Form::submit(($account->id) ? 'Update' : 'Create', ['class' => 'btn btn-success btn-centered']) !!}
|
||||
{!! Form::close() !!}
|
||||
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.main')
|
||||
@extends('layouts.main', ['large' => true])
|
||||
|
||||
@section('content')
|
||||
{{-- This view is only a wrapper for the markdown page --}}
|
||||
|
|
|
|||
|
|
@ -78,9 +78,19 @@ You can find more documentation on the related [IETF RFC-7616](https://tools.iet
|
|||
<span class="badge badge-success">Public</span>
|
||||
Returns `pong`
|
||||
|
||||
## Account Creation Request Tokens
|
||||
|
||||
An `account_creation_request_token` is a unique token that can be validated and then used to generate a valid `account_creation_token`.
|
||||
|
||||
### `POST /account_creation_request_tokens`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
||||
Create and return an `account_creation_request_token` that should then be validated to be used.
|
||||
|
||||
|
||||
## Account Creation Tokens
|
||||
|
||||
An account creation token is a unique token that allow the creation of a **unique** account.
|
||||
An `account_creation_token` is a unique token that allow the creation of a **unique** account.
|
||||
|
||||
### `POST /account_creation_tokens/send-by-push`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
|
@ -94,6 +104,16 @@ JSON parameters:
|
|||
* `pn_param` the push notification parameter
|
||||
* `pn_prid` the push notification unique id
|
||||
|
||||
### `POST /account_creation_tokens/using-account-creation-request-token`
|
||||
<span class="badge badge-success">Public</span>
|
||||
Create an `account_creation_token` using an `account_creation_request_token`.
|
||||
Return an `account_creation_token`.
|
||||
Return `404` if the `account_creation_request_token` provided is not valid or expired otherwise.
|
||||
|
||||
JSON parameters:
|
||||
|
||||
* `account_creation_request_token` required
|
||||
|
||||
### `POST /account_creation_tokens`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
|
||||
|
|
@ -134,6 +154,7 @@ JSON parameters:
|
|||
* `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
|
||||
* `account_creation_token` the unique `account_creation_token`
|
||||
|
||||
### `POST /accounts/with-account-creation-token`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
|
@ -146,7 +167,7 @@ JSON parameters:
|
|||
* `password` required minimum 6 characters
|
||||
* `algorithm` required, values can be `SHA-256` or `MD5`
|
||||
* `account_creation_token` the unique `account_creation_token`
|
||||
* `dtmf_protocol` optional, values must be `sipinfo` or `rfc2833`
|
||||
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
|
||||
|
||||
### `GET /accounts/{sip}/info`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
|
@ -162,6 +183,8 @@ Return `404` if the account doesn't exists.
|
|||
Retrieve public information about the account.
|
||||
Return `404` if the account doesn't exists.
|
||||
|
||||
Return `phone: true` if the returned account has a phone number.
|
||||
|
||||
### `POST /accounts/recover-by-phone`
|
||||
@if(config('app.dangerous_endpoints'))**Enabled on this instance**@else**Not enabled on this instance**@endif
|
||||
|
||||
|
|
@ -174,6 +197,7 @@ Return `404` if the account doesn't exists.
|
|||
JSON parameters:
|
||||
|
||||
* `phone` required the phone number to send the SMS to
|
||||
* `account_creation_token` the unique `account_creation_token`
|
||||
|
||||
### `GET /accounts/{sip}/recover/{recover_key}`
|
||||
@if(config('app.dangerous_endpoints'))**Enabled on this instance**@else**Not enabled on this instance**@endif
|
||||
|
|
@ -182,6 +206,9 @@ JSON parameters:
|
|||
<span class="badge badge-success">Public</span>
|
||||
<span class="badge badge-warning">Unsecure endpoint</span>
|
||||
Activate the account if the correct `recover_key` is provided.
|
||||
|
||||
The `sip` parameter can be the default SIP account or the phone based one.
|
||||
|
||||
Return the account information (including the hashed password) if valid.
|
||||
|
||||
Return `404` if the account doesn't exists.
|
||||
|
|
@ -192,7 +219,7 @@ Activate an account using a secret code received by email.
|
|||
Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
|
||||
JSON parameters:
|
||||
|
||||
* `code` the code
|
||||
* `confirmation_key` the confirmation key
|
||||
|
||||
### `POST /accounts/{sip}/activate/phone`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
|
@ -200,7 +227,7 @@ Activate an account using a pin code received by phone.
|
|||
Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
|
||||
JSON parameters:
|
||||
|
||||
* `code` the PIN code
|
||||
* `confirmation_key` the PIN code
|
||||
|
||||
### `GET /accounts/me/api_key/{auth_token}`
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
|
@ -261,9 +288,24 @@ JSON parameters:
|
|||
* `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
|
||||
* `dtmf_protocol` optional, values must be `sipinfo` or `rfc2833`
|
||||
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
|
||||
* `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`.
|
||||
|
||||
### `PUT /accounts/{id}`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Update an existing account.
|
||||
|
||||
JSON parameters:
|
||||
|
||||
* `username` unique username, minimum 6 characters
|
||||
* `password` required minimum 6 characters
|
||||
* `algorithm` required, values can be `SHA-256` or `MD5`
|
||||
* `display_name` optional, string
|
||||
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
|
||||
* `admin` optional, a boolean, set to `false` by default, create an admin account
|
||||
* `phone` optional, a phone number, set a phone number to the account
|
||||
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
|
||||
|
||||
### `GET /accounts`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Retrieve all the accounts, paginated.
|
||||
|
|
@ -272,10 +314,18 @@ Retrieve all the accounts, paginated.
|
|||
<span class="badge badge-warning">Admin</span>
|
||||
Retrieve a specific account.
|
||||
|
||||
### `POST /accounts/{id}/recover-by-email`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Send the account recovery email containing a fresh `provisioning_token` and `confirmation_key`
|
||||
|
||||
### `GET /accounts/{sip}/search`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Search for a specific account by sip address.
|
||||
|
||||
### `GET /accounts/{email}/search-by-email`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Search for a specific account by email.
|
||||
|
||||
### `DELETE /accounts/{id}`
|
||||
<span class="badge badge-warning">Admin</span>
|
||||
Delete a specific account and its related information.
|
||||
|
|
@ -447,25 +497,37 @@ The following URLs are **not API endpoints** they are not returning `JSON` conte
|
|||
|
||||
## Provisioning
|
||||
|
||||
When an account is having an available `provisioning_token` it can be provisioned using the two following URL.
|
||||
When an account is having an available `provisioning_token` it can be provisioned using the following URLs.
|
||||
|
||||
### `GET /provisioning`
|
||||
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
||||
Return the provisioning information available in the liblinphone configuration file (if correctly configured).
|
||||
|
||||
### `GET /provisioning/{provisioning_token}?reset_password`
|
||||
<span class="badge badge-success">Public</span>
|
||||
Return the provisioning information available in the liblinphone configuration file.
|
||||
If the `provisioning_token` is valid the related account information are added to the returned XML. The account is then considered as "provisioned" and those account related information will be removed in the upcoming requests (the content will be the same as the previous url).
|
||||
### `GET /provisioning/auth_token/{auth_token}`
|
||||
|
||||
If the account is not activated and the `provisioning_token` is valid. The account will be activated.
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
||||
Return the provisioning information available linked to the account that was attached to the `auth_token`.
|
||||
|
||||
### `GET /provisioning/{provisioning_token}?reset_password`
|
||||
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
||||
Return the provisioning information available linked to the account related to the `provisioning_token`.
|
||||
Return `404` if the `provisioning_token` provided is not valid or expired otherwise.
|
||||
|
||||
If the account is not activated the account will be activated. The account is then considered as "provisioned".
|
||||
|
||||
URL parameters:
|
||||
|
||||
* `reset_password` optional, reset the password while doing the provisioning
|
||||
|
||||
### `GET /provisioning/qrcode/{provisioning_token}?reset_password`
|
||||
|
||||
<span class="badge badge-success">Public</span>
|
||||
|
||||
Return a QRCode that points to the provisioning URL.
|
||||
|
||||
URL parameters:
|
||||
|
|
@ -473,7 +535,11 @@ URL parameters:
|
|||
* `reset_password` optional, reset the password while doing the provisioning
|
||||
|
||||
### `GET /provisioning/me`
|
||||
|
||||
<span class="badge badge-info">User</span>
|
||||
|
||||
Authenticated endpoint, see [API About & Auth]({{ route('api') }}#about--auth)
|
||||
|
||||
Return the same base content as the previous URL and the account related information, similar to the `provisioning_token` endpoint. However this endpoint will always return those information.
|
||||
|
||||
## Contacts list
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
@endsection
|
||||
|
||||
@section('body')
|
||||
<div class="container pt-4">
|
||||
<div class="container @if (isset($large) && $large) large @endif pt-4">
|
||||
@include('parts.errors')
|
||||
@yield('content')
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,69 +26,74 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
|
|||
});
|
||||
|
||||
Route::get('ping', 'Api\PingController@ping');
|
||||
Route::post('account_creation_tokens/send-by-push', 'Api\AccountCreationTokenController@sendByPush');
|
||||
// Old URL, for retro-compatibility
|
||||
Route::post('tokens', 'Api\AccountCreationTokenController@sendByPush');
|
||||
|
||||
Route::get('accounts/{sip}/info', 'Api\AccountController@info');
|
||||
Route::post('account_creation_request_tokens', 'Api\Account\CreationRequestToken@create');
|
||||
Route::post('account_creation_tokens/send-by-push', 'Api\Account\CreationTokenController@sendByPush');
|
||||
Route::post('account_creation_tokens/using-account-creation-request-token', 'Api\Account\CreationTokenController@usingAccountRequestToken');
|
||||
Route::post('accounts/with-account-creation-token', 'Api\Account\AccountController@store');
|
||||
|
||||
Route::post('accounts/with-account-creation-token', 'Api\AccountController@store');
|
||||
// Old URL, for retro-compatibility
|
||||
Route::post('accounts/with-token', 'Api\AccountController@store');
|
||||
Route::get('accounts/{sip}/info', 'Api\Account\AccountController@info');
|
||||
|
||||
Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmail');
|
||||
Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone');
|
||||
Route::post('accounts/{sip}/activate/email', 'Api\Account\AccountController@activateEmail');
|
||||
Route::post('accounts/{sip}/activate/phone', 'Api\Account\AccountController@activatePhone');
|
||||
|
||||
// /!\ Dangerous endpoints
|
||||
Route::post('accounts/public', 'Api\AccountController@storePublic');
|
||||
Route::get('accounts/{sip}/recover/{recovery_key}', 'Api\AccountController@recoverUsingKey');
|
||||
Route::post('accounts/recover-by-phone', 'Api\AccountController@recoverByPhone');
|
||||
Route::get('accounts/{phone}/info-by-phone', 'Api\AccountController@phoneInfo');
|
||||
Route::post('accounts/public', 'Api\Account\AccountController@storePublic');
|
||||
Route::get('accounts/{sip}/recover/{recovery_key}', 'Api\Account\AccountController@recoverUsingKey');
|
||||
Route::post('accounts/recover-by-phone', 'Api\Account\AccountController@recoverByPhone');
|
||||
Route::get('accounts/{phone}/info-by-phone', 'Api\Account\AccountController@phoneInfo');
|
||||
|
||||
Route::post('accounts/auth_token', 'Api\AuthTokenController@store');
|
||||
Route::post('accounts/auth_token', 'Api\Account\AuthTokenController@store');
|
||||
|
||||
Route::get('accounts/me/api_key/{auth_token}', 'Api\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt');
|
||||
Route::get('accounts/me/api_key/{auth_token}', 'Api\Account\ApiKeyController@generateFromToken')->middleware('cookie', 'cookie.encrypt');
|
||||
|
||||
Route::group(['middleware' => ['auth.digest_or_key']], function () {
|
||||
Route::get('statistic/month', 'Api\StatisticController@month');
|
||||
Route::get('statistic/week', 'Api\StatisticController@week');
|
||||
Route::get('statistic/day', 'Api\StatisticController@day');
|
||||
|
||||
Route::get('accounts/auth_token/{auth_token}/attach', 'Api\AuthTokenController@attach');
|
||||
Route::get('accounts/auth_token/{auth_token}/attach', 'Api\Account\AuthTokenController@attach');
|
||||
|
||||
Route::get('accounts/me/api_key', 'Api\ApiKeyController@generate')->middleware('cookie', 'cookie.encrypt');
|
||||
Route::get('accounts/me/api_key', 'Api\Account\ApiKeyController@generate')->middleware('cookie', 'cookie.encrypt');
|
||||
|
||||
Route::get('accounts/me', 'Api\AccountController@show');
|
||||
Route::delete('accounts/me', 'Api\AccountController@delete');
|
||||
Route::get('accounts/me/provision', 'Api\AccountController@provision');
|
||||
Route::get('accounts/me', 'Api\Account\AccountController@show');
|
||||
Route::delete('accounts/me', 'Api\Account\AccountController@delete');
|
||||
Route::get('accounts/me/provision', 'Api\Account\AccountController@provision');
|
||||
|
||||
Route::post('accounts/me/phone/request', 'Api\AccountPhoneController@requestUpdate');
|
||||
Route::post('accounts/me/phone', 'Api\AccountPhoneController@update');
|
||||
Route::post('accounts/me/phone/request', 'Api\Account\PhoneController@requestUpdate');
|
||||
Route::post('accounts/me/phone', 'Api\Account\PhoneController@update');
|
||||
|
||||
Route::get('accounts/me/devices', 'Api\DeviceController@index');
|
||||
Route::delete('accounts/me/devices/{uuid}', 'Api\DeviceController@destroy');
|
||||
Route::get('accounts/me/devices', 'Api\Account\DeviceController@index');
|
||||
Route::delete('accounts/me/devices/{uuid}', 'Api\Account\DeviceController@destroy');
|
||||
|
||||
Route::post('accounts/me/email/request', 'Api\EmailController@requestUpdate');
|
||||
Route::post('accounts/me/password', 'Api\PasswordController@update');
|
||||
Route::post('accounts/me/email/request', 'Api\Account\EmailController@requestUpdate');
|
||||
Route::post('accounts/me/password', 'Api\Account\PasswordController@update');
|
||||
|
||||
Route::get('accounts/me/contacts/{sip}', 'Api\AccountContactController@show');
|
||||
Route::get('accounts/me/contacts', 'Api\AccountContactController@index');
|
||||
Route::get('accounts/me/contacts/{sip}', 'Api\Account\ContactController@show');
|
||||
Route::get('accounts/me/contacts', 'Api\Account\ContactController@index');
|
||||
|
||||
Route::group(['middleware' => ['auth.admin']], function () {
|
||||
if (!empty(config('app.linphone_daemon_unix_pipe'))) {
|
||||
Route::post('messages', 'Api\MessageController@send');
|
||||
Route::post('messages', 'Api\Admin\MessageController@send');
|
||||
}
|
||||
|
||||
// Account creation token
|
||||
Route::post('account_creation_tokens', 'Api\Admin\AccountCreationTokenController@create');
|
||||
|
||||
// Accounts
|
||||
Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate');
|
||||
Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate');
|
||||
Route::get('accounts/{id}/provision', 'Api\Admin\AccountController@provision');
|
||||
|
||||
Route::post('accounts/{id}/recover-by-email', 'Api\Admin\AccountController@recoverByEmail');
|
||||
|
||||
Route::post('accounts', 'Api\Admin\AccountController@store');
|
||||
Route::put('accounts/{id}', 'Api\Admin\AccountController@update');
|
||||
Route::get('accounts', 'Api\Admin\AccountController@index');
|
||||
Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search');
|
||||
Route::get('accounts/{id}', 'Api\Admin\AccountController@show');
|
||||
Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy');
|
||||
Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search');
|
||||
Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail');
|
||||
|
||||
// Account actions
|
||||
Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index');
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use \Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', 'Account\AccountController@home')->name('account.home');
|
||||
Route::get('documentation', 'Account\AccountController@documentation')->name('account.documentation');
|
||||
|
||||
|
|
@ -29,13 +31,18 @@ if (config('app.web_panel')) {
|
|||
Route::get('authenticate/email/check/{sip}', 'Account\AuthenticateController@checkEmail')->name('account.check.email');
|
||||
Route::get('authenticate/email/{code}', 'Account\AuthenticateController@validateEmail')->name('account.authenticate.email_confirm');
|
||||
|
||||
Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone');
|
||||
Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone');
|
||||
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm');
|
||||
if (config('app.phone_authentication')) {
|
||||
Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone');
|
||||
Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone');
|
||||
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm');
|
||||
}
|
||||
|
||||
Route::get('authenticate/qrcode/{token?}', 'Account\AuthenticateController@loginAuthToken')->name('account.authenticate.auth_token');
|
||||
}
|
||||
|
||||
Route::get('creation_token/check/{token}', 'Account\CreationRequestTokenController@check')->name('account.creation_request_token.check');
|
||||
Route::post('creation_token/validate', 'Account\CreationRequestTokenController@validateToken')->name('account.creation_request_token.validate');
|
||||
|
||||
Route::group(['middleware' => 'auth.digest_or_key'], function () {
|
||||
Route::get('provisioning/me', 'Account\ProvisioningController@me')->name('provisioning.me');
|
||||
|
||||
|
|
@ -44,9 +51,13 @@ Route::group(['middleware' => 'auth.digest_or_key'], function () {
|
|||
Route::get('contacts/vcard', 'Account\ContactVcardController@index')->name('account.contacts.vcard.index');
|
||||
});
|
||||
|
||||
Route::get('provisioning/auth_token/{auth_token}', 'Account\ProvisioningController@authToken')->name('provisioning.auth_token');
|
||||
Route::get('provisioning/qrcode/{provisioning_token}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode');
|
||||
Route::get('provisioning/{provisioning_token?}', 'Account\ProvisioningController@show')->name('provisioning.show');
|
||||
Route::name('provisioning.')->prefix('provisioning')->controller('Account\ProvisioningController')->group(function () {
|
||||
Route::get('documentation', 'documentation')->name('documentation');
|
||||
Route::get('auth_token/{auth_token}', 'authToken')->name('auth_token');
|
||||
Route::get('qrcode/{provisioning_token}', 'qrcode')->name('qrcode');
|
||||
Route::get('{provisioning_token}', 'provision')->name('provision');
|
||||
Route::get('/', 'show')->name('show');
|
||||
});
|
||||
|
||||
if (publicRegistrationEnabled()) {
|
||||
if (config('app.phone_authentication')) {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class AccountProvisioningTest extends TestCase
|
|||
$currentPassword = $password->password;
|
||||
|
||||
$provioningUrl = route(
|
||||
'provisioning.show',
|
||||
'provisioning.provision',
|
||||
[
|
||||
'provisioning_token' => $password->account->provisioning_token,
|
||||
'reset_password' => true
|
||||
|
|
@ -132,9 +132,7 @@ class AccountProvisioningTest extends TestCase
|
|||
public function testConfirmationKeyProvisioning()
|
||||
{
|
||||
$response = $this->get($this->route . '/1234');
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Content-Type', 'application/xml');
|
||||
$response->assertDontSee('ha1');
|
||||
$response->assertStatus(404);
|
||||
|
||||
$password = Password::factory()->create();
|
||||
$password->account->generateApiKey();
|
||||
|
|
@ -152,9 +150,7 @@ class AccountProvisioningTest extends TestCase
|
|||
|
||||
// And then twice
|
||||
$response = $this->get($this->route . '/' . $password->account->provisioning_token)
|
||||
->assertStatus(200)
|
||||
->assertHeader('Content-Type', 'application/xml')
|
||||
->assertDontSee('ha1');
|
||||
->assertStatus(404);
|
||||
|
||||
$password->account->refresh();
|
||||
|
||||
|
|
@ -188,9 +184,9 @@ class AccountProvisioningTest extends TestCase
|
|||
->assertStatus(201)
|
||||
->assertJson([
|
||||
'token' => true
|
||||
])->content();
|
||||
]);
|
||||
|
||||
$authToken = json_decode($response)->token;
|
||||
$authToken = $response->json('token');
|
||||
|
||||
$password = Password::factory()->create();
|
||||
$password->account->generateApiKey();
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ class ApiAccountApiKeyTest extends TestCase
|
|||
->assertStatus(201)
|
||||
->assertJson([
|
||||
'token' => true
|
||||
])->content();
|
||||
]);
|
||||
|
||||
$authToken = json_decode($response)->token;
|
||||
$authToken = $response->json('token');
|
||||
|
||||
// Try to retrieve an API key from the un-attached auth_token
|
||||
$response = $this->json($this->method, $this->route . '/' . $authToken)
|
||||
|
|
@ -95,9 +95,9 @@ class ApiAccountApiKeyTest extends TestCase
|
|||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'api_key' => true
|
||||
])->content();
|
||||
]);
|
||||
|
||||
$apiKey = json_decode($response)->api_key;
|
||||
$apiKey = $response->json('api_key');
|
||||
|
||||
// Re-retrieve
|
||||
$this->json($this->method, $this->route . '/' . $authToken)
|
||||
|
|
@ -106,8 +106,7 @@ class ApiAccountApiKeyTest extends TestCase
|
|||
// Check the if the API key can be used for the account
|
||||
$response = $this->withHeaders(['x-api-key' => $apiKey])
|
||||
->json($this->method, '/api/accounts/me')
|
||||
->assertStatus(200)
|
||||
->content();
|
||||
->assertStatus(200);
|
||||
|
||||
// Try with a wrong From
|
||||
$response = $this->withHeaders([
|
||||
|
|
@ -115,10 +114,9 @@ class ApiAccountApiKeyTest extends TestCase
|
|||
'From' => 'sip:baduser@server.tld'
|
||||
])
|
||||
->json($this->method, '/api/accounts/me')
|
||||
->assertStatus(200)
|
||||
->content();
|
||||
->assertStatus(200);
|
||||
|
||||
// Check if the account was correctly attached
|
||||
$this->assertEquals(json_decode($response)->email, $password->account->email);
|
||||
$this->assertEquals($response->json('email'), $password->account->email);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,84 +19,93 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Account;
|
||||
use App\AccountCreationRequestToken;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
use App\AccountCreationToken;
|
||||
use App\Admin;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ApiAccountCreationTokenTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $tokenRoute = '/api/account_creation_tokens/send-by-push';
|
||||
protected $tokenRequestRoute = '/api/account_creation_request_tokens';
|
||||
protected $tokenUsingCreationTokenRoute = '/api/account_creation_tokens/using-account-creation-request-token';
|
||||
protected $accountRoute = '/api/accounts/with-account-creation-token';
|
||||
protected $adminRoute = '/api/account_creation_tokens';
|
||||
protected $method = 'POST';
|
||||
|
||||
protected $pnProvider = 'provider';
|
||||
protected $pnParam = 'param';
|
||||
protected $pnPrid = 'id';
|
||||
|
||||
public function testMandatoryParameters()
|
||||
{
|
||||
$response = $this->json($this->method, $this->tokenRoute);
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
public function testCorrectParameters()
|
||||
{
|
||||
$response = $this->json($this->method, $this->tokenRoute, [
|
||||
$this->assertSame(AccountCreationToken::count(), 0);
|
||||
$this->json($this->method, $this->tokenRoute, [
|
||||
'pn_provider' => $this->pnProvider,
|
||||
'pn_param' => $this->pnParam,
|
||||
'pn_prid' => $this->pnPrid,
|
||||
])->assertStatus(503);
|
||||
}
|
||||
|
||||
public function testMandatoryParameters()
|
||||
{
|
||||
$this->json($this->method, $this->tokenRoute)->assertStatus(422);
|
||||
|
||||
$this->json($this->method, $this->tokenRoute, [
|
||||
'pn_provider' => null,
|
||||
'pn_param' => null,
|
||||
'pn_prid' => null,
|
||||
])->assertStatus(422);
|
||||
}
|
||||
|
||||
public function testThrottling()
|
||||
{
|
||||
AccountCreationToken::factory()->create([
|
||||
'pn_provider' => $this->pnProvider,
|
||||
'pn_param' => $this->pnParam,
|
||||
'pn_prid' => $this->pnPrid,
|
||||
]);
|
||||
$response->assertStatus(503);
|
||||
|
||||
$this->json($this->method, $this->tokenRoute, [
|
||||
'pn_provider' => $this->pnProvider,
|
||||
'pn_param' => $this->pnParam,
|
||||
'pn_prid' => $this->pnPrid,
|
||||
])->assertStatus(503);
|
||||
|
||||
// Redeem all the tokens
|
||||
AccountCreationToken::where('used', false)->update(['used' => true]);
|
||||
|
||||
$this->json($this->method, $this->tokenRoute, [
|
||||
'pn_provider' => $this->pnProvider,
|
||||
'pn_param' => $this->pnParam,
|
||||
'pn_prid' => $this->pnPrid,
|
||||
])->assertStatus(429);
|
||||
}
|
||||
|
||||
/**
|
||||
* For retro-compatibility only
|
||||
*/
|
||||
public function testRetrocopatibilityToken()
|
||||
public function testAdminEndpoint()
|
||||
{
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
$admin = Admin::factory()->create();
|
||||
$admin->account->generateApiKey();
|
||||
|
||||
$response = $this->json($this->method, '/api/tokens', [
|
||||
'pn_provider' => $token->pn_provider,
|
||||
'pn_param' => $token->pn_param,
|
||||
'pn_prid' => $token->pn_prid
|
||||
$response = $this->keyAuthenticated($admin->account)
|
||||
->json($this->method, $this->adminRoute)
|
||||
->assertStatus(201);
|
||||
|
||||
$this->assertDatabaseHas('account_creation_tokens', [
|
||||
'token' => $response->json()['token']
|
||||
]);
|
||||
$response->assertStatus(503);
|
||||
}
|
||||
|
||||
public function testInvalidToken()
|
||||
{
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
||||
// Valid token
|
||||
$response = $this->json($this->method, '/api/accounts/with-token', [
|
||||
'username' => 'username',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'token' => $token->token
|
||||
]);
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Expired token
|
||||
$response = $this->json($this->method, '/api/accounts/with-token', [
|
||||
'username' => 'username2',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'token' => $token->token
|
||||
]);
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* For retrocompatibility only
|
||||
*/
|
||||
public function testRetrocompatibilityInvalidToken()
|
||||
{
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
||||
// Invalid token
|
||||
$response = $this->json($this->method, $this->accountRoute, [
|
||||
'username' => 'username',
|
||||
|
|
@ -123,11 +132,13 @@ class ApiAccountCreationTokenTest extends TestCase
|
|||
'account_creation_token' => $token->token
|
||||
]);
|
||||
$response->assertStatus(422);
|
||||
|
||||
$this->assertDatabaseHas('account_creation_tokens', [
|
||||
'used' => true,
|
||||
'account_id' => Account::where('username', 'username')->first()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test username blacklist
|
||||
*/
|
||||
public function testBlacklistedUsername()
|
||||
{
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
|
@ -165,4 +176,36 @@ class ApiAccountCreationTokenTest extends TestCase
|
|||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
public function testAccountCreationRequestToken()
|
||||
{
|
||||
$response = $this->json($this->method, $this->tokenRequestRoute);
|
||||
$response->assertStatus(201);
|
||||
$creationRequestToken = $response->json()['token'];
|
||||
|
||||
$this->assertSame($response->json()['validation_url'], route('account.creation_request_token.check', $creationRequestToken));
|
||||
|
||||
// Validate the creation request token
|
||||
AccountCreationRequestToken::where('token', $creationRequestToken)->update(['validated_at' => Carbon::now()]);
|
||||
|
||||
$response = $this->json($this->method, $this->tokenUsingCreationTokenRoute, [
|
||||
'account_creation_request_token' => $creationRequestToken
|
||||
])->assertStatus(201);
|
||||
|
||||
$creationToken = $response->json()['token'];
|
||||
|
||||
$this->assertDatabaseHas('account_creation_request_tokens', [
|
||||
'token' => $creationRequestToken,
|
||||
'used' => true
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('account_creation_tokens', [
|
||||
'token' => $creationToken
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
AccountCreationRequestToken::where('token', $creationRequestToken)->first()->accountCreationToken->id,
|
||||
AccountCreationToken::where('token', $creationToken)->first()->id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace Tests\Feature;
|
|||
|
||||
use App\Password;
|
||||
use App\Account;
|
||||
use App\AccountCreationToken;
|
||||
use App\AccountTombstone;
|
||||
use App\ActivationExpiration;
|
||||
use App\Admin;
|
||||
|
|
@ -49,7 +50,7 @@ class ApiAccountTest extends TestCase
|
|||
$password = Password::factory()->create();
|
||||
$response0 = $this->generateFirstResponse($password);
|
||||
$response1 = $this->generateSecondResponse($password, $response0)
|
||||
->json($this->method, $this->route);
|
||||
->json($this->method, $this->route);
|
||||
|
||||
$response1->assertStatus(403);
|
||||
}
|
||||
|
|
@ -90,12 +91,12 @@ class ApiAccountTest extends TestCase
|
|||
$domain = 'example.com';
|
||||
|
||||
$response = $this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
]);
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
]);
|
||||
|
||||
$response->assertJsonValidationErrors(['username']);
|
||||
}
|
||||
|
|
@ -110,28 +111,35 @@ class ApiAccountTest extends TestCase
|
|||
$username = 'blabla🔥';
|
||||
$domain = 'example.com';
|
||||
|
||||
$response = $this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
]);
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
])->assertJsonValidationErrors(['username']);
|
||||
|
||||
$response->assertJsonValidationErrors(['username']);
|
||||
// Change the regex
|
||||
config()->set('app.account_username_regex', '^[a-z0-9🔥+_.-]*$');
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
])->assertStatus(200);
|
||||
|
||||
$username = 'blabla hop';
|
||||
$domain = 'example.com';
|
||||
|
||||
$response = $this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
]);
|
||||
|
||||
$response->assertJsonValidationErrors(['username']);
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'domain' => $domain,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '123456',
|
||||
])->assertJsonValidationErrors(['username']);
|
||||
}
|
||||
|
||||
public function testDomain()
|
||||
|
|
@ -318,7 +326,7 @@ class ApiAccountTest extends TestCase
|
|||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'password' => 'blabla',
|
||||
'admin' => true,
|
||||
]);
|
||||
|
||||
|
|
@ -348,7 +356,7 @@ class ApiAccountTest extends TestCase
|
|||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'password' => 'blabla',
|
||||
'activated' => true,
|
||||
]);
|
||||
|
||||
|
|
@ -378,7 +386,7 @@ class ApiAccountTest extends TestCase
|
|||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'password' => 'blabla',
|
||||
'activated' => false,
|
||||
]);
|
||||
|
||||
|
|
@ -407,7 +415,7 @@ class ApiAccountTest extends TestCase
|
|||
/**
|
||||
* Public information
|
||||
*/
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/info')
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/info')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false,
|
||||
|
|
@ -421,7 +429,7 @@ class ApiAccountTest extends TestCase
|
|||
* Retrieve the authenticated account
|
||||
*/
|
||||
$this->keyAuthenticated($password->account)
|
||||
->get($this->route.'/me')
|
||||
->get($this->route . '/me')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $password->account->username,
|
||||
|
|
@ -433,14 +441,14 @@ class ApiAccountTest extends TestCase
|
|||
* Retrieve the authenticated account
|
||||
*/
|
||||
$this->keyAuthenticated($password->account)
|
||||
->delete($this->route.'/me')
|
||||
->delete($this->route . '/me')
|
||||
->assertStatus(200);
|
||||
|
||||
/**
|
||||
* Check again
|
||||
*/
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/info')
|
||||
->assertStatus(404);
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/info')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
public function testActivateEmail()
|
||||
|
|
@ -457,36 +465,36 @@ class ApiAccountTest extends TestCase
|
|||
$expiration->expires = Carbon::now()->subYear();
|
||||
$expiration->save();
|
||||
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/info')
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/info')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/blabla/activate/email', [
|
||||
'code' => $confirmationKey
|
||||
->json($this->method, $this->route . '/blabla/activate/email', [
|
||||
'confirmation_key' => $confirmationKey
|
||||
])
|
||||
->assertStatus(404);
|
||||
|
||||
$activateEmailRoute = $this->route.'/'.$password->account->identifier.'/activate/email';
|
||||
$activateEmailRoute = $this->route . '/' . $password->account->identifier . '/activate/email';
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $activateEmailRoute, [
|
||||
'code' => $confirmationKey.'longer'
|
||||
'confirmation_key' => $confirmationKey . 'longer'
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $activateEmailRoute, [
|
||||
'code' => 'X123456789abc'
|
||||
'confirmation_key' => 'X123456789abc'
|
||||
])
|
||||
->assertStatus(404);
|
||||
|
||||
// Expired
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $activateEmailRoute, [
|
||||
'code' => $confirmationKey
|
||||
'confirmation_key' => $confirmationKey
|
||||
])
|
||||
->assertStatus(403);
|
||||
|
||||
|
|
@ -494,11 +502,11 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $activateEmailRoute, [
|
||||
'code' => $confirmationKey
|
||||
'confirmation_key' => $confirmationKey
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/info')
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/info')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true
|
||||
|
|
@ -532,6 +540,75 @@ class ApiAccountTest extends TestCase
|
|||
->assertJsonValidationErrors(['email']);
|
||||
}
|
||||
|
||||
public function testNonAsciiPasswordAdmin()
|
||||
{
|
||||
$admin = Admin::factory()->create();
|
||||
$admin->account->generateApiKey();
|
||||
$admin->account->save();
|
||||
|
||||
$username = 'username';
|
||||
|
||||
$response = $this->generateFirstResponse($admin->account->passwords()->first(), $this->method, $this->route);
|
||||
$this->generateSecondResponse($admin->account->passwords()->first(), $response)
|
||||
->json($this->method, $this->route, [
|
||||
'username' => $username,
|
||||
'email' => 'email@test.com',
|
||||
'domain' => 'server.com',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => 'nonascii€',
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
$password = Account::where('username', $username)->first()->passwords()->first();
|
||||
|
||||
$response = $this->generateFirstResponse($password, 'GET', '/api/accounts/me');
|
||||
$response = $this->generateSecondResponse($password, $response)
|
||||
->json('GET', '/api/accounts/me');
|
||||
}
|
||||
|
||||
public function testEditAdmin()
|
||||
{
|
||||
$password = Password::factory()->create();
|
||||
$account = $password->account;
|
||||
|
||||
$admin = Admin::factory()->create();
|
||||
$admin->account->generateApiKey();
|
||||
$admin->account->save();
|
||||
|
||||
$username = 'changed';
|
||||
$algorithm = 'MD5';
|
||||
$password = 'other';
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json('PUT', $this->route . '/1234')
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username']);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json('PUT', $this->route . '/1234', [
|
||||
'username' => 'good'
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json('PUT', $this->route . '/' . $account->id, [
|
||||
'username' => $username,
|
||||
'algorithm' => $algorithm,
|
||||
'password' => $password,
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'id' => $account->id,
|
||||
'username' => $username
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('passwords', [
|
||||
'account_id' => $account->id,
|
||||
'algorithm' => $algorithm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* /!\ Dangerous endpoints
|
||||
*/
|
||||
|
|
@ -552,21 +629,44 @@ class ApiAccountTest extends TestCase
|
|||
'activated' => false
|
||||
]);
|
||||
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/recover/'.$confirmationKey)
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
|
||||
->assertJson(['passwords' => [[
|
||||
'password' => $password->password,
|
||||
'algorithm' => $password->algorithm
|
||||
]]])
|
||||
->assertStatus(200);
|
||||
|
||||
$this->json('GET', $this->route.'/'.$password->account->identifier.'/recover/'.$confirmationKey)
|
||||
$this->json('GET', $this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'username' => $password->account->username,
|
||||
'domain' => $password->account->domain,
|
||||
'confirmation_key' => null,
|
||||
'activated' => true
|
||||
]);
|
||||
|
||||
// Recover by alias
|
||||
|
||||
$newConfirmationKey = '1345';
|
||||
|
||||
$password->account->confirmation_key = $newConfirmationKey;
|
||||
$password->account->save();
|
||||
|
||||
$phone = '+1234';
|
||||
|
||||
$alias = new AppAlias;
|
||||
$alias->alias = $phone;
|
||||
$alias->domain = $password->account->domain;
|
||||
$alias->account_id = $password->account->id;
|
||||
$alias->save();
|
||||
|
||||
$this->get($this->route . '/' . $phone . '@' . $alias->domain . '/recover/' . $newConfirmationKey)
|
||||
->assertJson(['passwords' => [[
|
||||
'password' => $password->password,
|
||||
'algorithm' => $password->algorithm
|
||||
]]])
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -589,36 +689,79 @@ class ApiAccountTest extends TestCase
|
|||
$alias->account_id = $password->account->id;
|
||||
$alias->save();
|
||||
|
||||
$this->json($this->method, $this->route.'/recover-by-phone', [
|
||||
'phone' => $phone
|
||||
])
|
||||
$this->json($this->method, $this->route . '/recover-by-phone', [
|
||||
'phone' => $phone
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['account_creation_token']);
|
||||
|
||||
|
||||
$this->json($this->method, $this->route . '/recover-by-phone', [
|
||||
'phone' => $phone,
|
||||
'account_creation_token' => 'wrong'
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['account_creation_token']);
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
||||
$this->json($this->method, $this->route . '/recover-by-phone', [
|
||||
'phone' => $phone,
|
||||
'account_creation_token' => $token->token
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
$password->account->refresh();
|
||||
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/recover/'.$password->account->confirmation_key)
|
||||
// Use the token a second time
|
||||
$this->json($this->method, $this->route . '/recover-by-phone', [
|
||||
'phone' => $phone,
|
||||
'account_creation_token' => $token->token
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/recover/' . $password->account->confirmation_key)
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true
|
||||
]);
|
||||
|
||||
$this->get($this->route.'/'.$phone.'/info-by-phone')
|
||||
$this->get($this->route . '/' . $phone . '/info-by-phone')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true
|
||||
'activated' => true,
|
||||
'phone' => true
|
||||
]);
|
||||
|
||||
$this->get($this->route.'/+1234/info-by-phone')
|
||||
$this->get($this->route . '/+1234/info-by-phone')
|
||||
->assertStatus(404);
|
||||
|
||||
$this->json('GET', $this->route.'/'.$password->account->identifier.'/info-by-phone')
|
||||
$this->json('GET', $this->route . '/' . $password->account->identifier . '/info-by-phone')
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
|
||||
// Check the mixed username/phone resolution...
|
||||
$password->account->username = $phone;
|
||||
$password->account->save();
|
||||
|
||||
$alias->delete();
|
||||
|
||||
$this->get($this->route . '/' . $phone . '/info-by-phone')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true,
|
||||
'phone' => false
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('account_creation_tokens', [
|
||||
'used' => true,
|
||||
'account_id' => $password->account->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* /!\ Dangerous endpoints
|
||||
*/
|
||||
|
||||
public function testCreatePublic()
|
||||
{
|
||||
$username = 'publicuser';
|
||||
|
|
@ -626,50 +769,83 @@ class ApiAccountTest extends TestCase
|
|||
config()->set('app.dangerous_endpoints', true);
|
||||
|
||||
// Missing email
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['account_creation_token']);
|
||||
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
$userAgent = 'User Agent Test';
|
||||
|
||||
$this->withHeaders([
|
||||
'User-Agent' => $userAgent,
|
||||
])->json($this->method, $this->route . '/public', [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
'account_creation_token' => $token->token
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
|
||||
// Re-use the token
|
||||
$this->withHeaders([
|
||||
'User-Agent' => $userAgent,
|
||||
])->json($this->method, $this->route . '/public', [
|
||||
'username' => $username . 'foo',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
'account_creation_token' => $token->token
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
// Already created
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'username' => $username,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username']);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username']);
|
||||
|
||||
// Email is now unique
|
||||
config()->set('app.account_email_unique', true);
|
||||
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'username' => 'johndoe',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'username' => $username,
|
||||
'domain' => config('app.sip_domain')
|
||||
'domain' => config('app.sip_domain'),
|
||||
'user_agent' => $userAgent
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('account_creation_tokens', [
|
||||
'used' => true,
|
||||
'account_id' => Account::where('username', $username)->first()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -679,47 +855,39 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
config()->set('app.dangerous_endpoints', true);
|
||||
|
||||
// Username and phone
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
'username' => 'myusername',
|
||||
'phone' => $phone,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone', 'username']);
|
||||
|
||||
// Bad phone format
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'phone' => 'username',
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$token = AccountCreationToken::factory()->create();
|
||||
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'phone' => $phone,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
'account_creation_token' => $token->token
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
|
||||
// Already exists
|
||||
$this->json($this->method, $this->route.'/public', [
|
||||
$this->json($this->method, $this->route . '/public', [
|
||||
'phone' => $phone,
|
||||
'algorithm' => 'SHA-256',
|
||||
'password' => '2',
|
||||
'email' => 'john@doe.tld',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'username' => $phone,
|
||||
|
|
@ -746,7 +914,7 @@ class ApiAccountTest extends TestCase
|
|||
$expiration->expires = Carbon::now()->subYear();
|
||||
$expiration->save();
|
||||
|
||||
$this->get($this->route.'/'.$password->account->identifier.'/info')
|
||||
$this->get($this->route . '/' . $password->account->identifier . '/info')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
|
|
@ -754,16 +922,16 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Expired
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [
|
||||
'code' => $confirmationKey
|
||||
->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
|
||||
'confirmation_key' => $confirmationKey
|
||||
])
|
||||
->assertStatus(403);
|
||||
|
||||
$expiration->delete();
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [
|
||||
'code' => $confirmationKey
|
||||
->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
|
||||
'confirmation_key' => $confirmationKey
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
|
|
@ -783,27 +951,27 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Bad email
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/me/email/request', [
|
||||
->json($this->method, $this->route . '/me/email/request', [
|
||||
'email' => 'gnap'
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
// Same email
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/me/email/request', [
|
||||
->json($this->method, $this->route . '/me/email/request', [
|
||||
'email' => $password->account->email
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
// Correct email
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/me/email/request', [
|
||||
->json($this->method, $this->route . '/me/email/request', [
|
||||
'email' => $newEmail
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->get($this->route.'/me')
|
||||
->get($this->route . '/me')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $password->account->username,
|
||||
|
|
@ -816,7 +984,7 @@ class ApiAccountTest extends TestCase
|
|||
config()->set('app.account_email_unique', true);
|
||||
|
||||
$this->keyAuthenticated($password->account)
|
||||
->json($this->method, $this->route.'/me/email/request', [
|
||||
->json($this->method, $this->route . '/me/email/request', [
|
||||
'email' => $otherAccount->account->email
|
||||
])
|
||||
->assertStatus(422)
|
||||
|
|
@ -834,7 +1002,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Wrong algorithm
|
||||
$this->keyAuthenticated($account)
|
||||
->json($this->method, $this->route.'/me/password', [
|
||||
->json($this->method, $this->route . '/me/password', [
|
||||
'algorithm' => '123',
|
||||
'password' => $password
|
||||
])
|
||||
|
|
@ -843,7 +1011,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Fresh password without an old one
|
||||
$this->keyAuthenticated($account)
|
||||
->json($this->method, $this->route.'/me/password', [
|
||||
->json($this->method, $this->route . '/me/password', [
|
||||
'algorithm' => $algorithm,
|
||||
'password' => $password
|
||||
])
|
||||
|
|
@ -851,7 +1019,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// First check
|
||||
$this->keyAuthenticated($account)
|
||||
->get($this->route.'/me')
|
||||
->get($this->route . '/me')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $account->username,
|
||||
|
|
@ -862,7 +1030,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Set new password without old one
|
||||
$this->keyAuthenticated($account)
|
||||
->json($this->method, $this->route.'/me/password', [
|
||||
->json($this->method, $this->route . '/me/password', [
|
||||
'algorithm' => $newAlgorithm,
|
||||
'password' => $newPassword
|
||||
])
|
||||
|
|
@ -871,7 +1039,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Set the new password with incorrect old password
|
||||
$this->keyAuthenticated($account)
|
||||
->json($this->method, $this->route.'/me/password', [
|
||||
->json($this->method, $this->route . '/me/password', [
|
||||
'algorithm' => $newAlgorithm,
|
||||
'old_password' => 'blabla',
|
||||
'password' => $newPassword
|
||||
|
|
@ -881,7 +1049,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Set the new password
|
||||
$this->keyAuthenticated($account)
|
||||
->json($this->method, $this->route.'/me/password', [
|
||||
->json($this->method, $this->route . '/me/password', [
|
||||
'algorithm' => $newAlgorithm,
|
||||
'old_password' => $password,
|
||||
'password' => $newPassword
|
||||
|
|
@ -890,7 +1058,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Second check
|
||||
$this->keyAuthenticated($account)
|
||||
->get($this->route.'/me')
|
||||
->get($this->route . '/me')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'username' => $account->username,
|
||||
|
|
@ -909,28 +1077,28 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// deactivate
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->id.'/deactivate')
|
||||
->get($this->route . '/' . $password->account->id . '/deactivate')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->id)
|
||||
->get($this->route . '/' . $password->account->id)
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => false
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->id.'/activate')
|
||||
->get($this->route . '/' . $password->account->id . '/activate')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->id)
|
||||
->get($this->route . '/' . $password->account->id)
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'activated' => true
|
||||
|
|
@ -938,12 +1106,44 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// Search feature
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->identifier.'/search')
|
||||
->get($this->route . '/' . $password->account->identifier . '/search')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'id' => $password->account->id,
|
||||
'activated' => true
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route . '/' . $password->account->email . '/search-by-email')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'id' => $password->account->id,
|
||||
'activated' => true
|
||||
]);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route . '/wrong@email.com/search-by-email')
|
||||
->assertStatus(404);
|
||||
}
|
||||
|
||||
public function testRecoverByEmail()
|
||||
{
|
||||
$email = 'collision@email.com';
|
||||
|
||||
$account = Password::factory()->create();
|
||||
$account->account->email = $email;
|
||||
$account->account->save();
|
||||
|
||||
$admin = Admin::factory()->create();
|
||||
$admin->account->generateApiKey();
|
||||
$admin->account->save();
|
||||
|
||||
$response = $this->keyAuthenticated($admin->account)
|
||||
->post($this->route . '/' . $account->id . '/recover-by-email')
|
||||
->assertStatus(200);
|
||||
|
||||
$this->assertNotEquals($response->json('confirmation_key'), $account->confirmation_key);
|
||||
$this->assertNotEquals($response->json('provisioning_token'), $account->provisioning_token);
|
||||
}
|
||||
|
||||
public function testGetAll()
|
||||
|
|
@ -963,7 +1163,7 @@ class ApiAccountTest extends TestCase
|
|||
|
||||
// /accounts/id
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$admin->id)
|
||||
->get($this->route . '/' . $admin->id)
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'id' => 1,
|
||||
|
|
@ -1024,13 +1224,13 @@ class ApiAccountTest extends TestCase
|
|||
$admin->account->generateApiKey();
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->delete($this->route.'/'.$password->account->id)
|
||||
->delete($this->route . '/' . $password->account->id)
|
||||
->assertStatus(200);
|
||||
|
||||
$this->assertEquals(1, AccountTombstone::count());
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route.'/'.$password->account->id)
|
||||
->get($this->route . '/' . $password->account->id)
|
||||
->assertStatus(404);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,17 +47,26 @@ class ApiAccountTypeTest extends TestCase
|
|||
|
||||
$this->assertEquals(1, AccountType::count());
|
||||
|
||||
// Same key
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json($this->method, $this->route, [
|
||||
'key' => 'phone',
|
||||
])
|
||||
->assertJsonValidationErrorFor('key')
|
||||
->assertStatus(422);
|
||||
|
||||
|
||||
// Missing key
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json($this->method, $this->route, [])
|
||||
->assertStatus(422);
|
||||
->json($this->method, $this->route, [])
|
||||
->assertStatus(422);
|
||||
|
||||
// Invalid key
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json($this->method, $this->route, [
|
||||
'key' => 'Abc1234',
|
||||
])
|
||||
->assertStatus(422);
|
||||
->json($this->method, $this->route, [
|
||||
'key' => 'Abc1234',
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get($this->route)
|
||||
|
|
@ -83,7 +92,7 @@ class ApiAccountTypeTest extends TestCase
|
|||
$accountType = AccountType::first();
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->delete($this->route.'/'.$accountType->id)
|
||||
->delete($this->route . '/' . $accountType->id)
|
||||
->assertStatus(200);
|
||||
|
||||
$this->assertEquals(0, AccountType::count());
|
||||
|
|
@ -104,7 +113,7 @@ class ApiAccountTypeTest extends TestCase
|
|||
$accountType = AccountType::first();
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json('PUT', $this->route.'/'.$accountType->id, [
|
||||
->json('PUT', $this->route . '/' . $accountType->id, [
|
||||
'key' => 'door',
|
||||
])
|
||||
->assertStatus(200);
|
||||
|
|
@ -137,34 +146,34 @@ class ApiAccountTypeTest extends TestCase
|
|||
$password = Password::factory()->create();
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json($this->method, '/api/accounts/'.$password->account->id.'/types/'.$accountType->id)
|
||||
->json($this->method, '/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
|
||||
->assertStatus(200);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->json($this->method, '/api/accounts/'.$password->account->id.'/types/'.$accountType->id)
|
||||
->json($this->method, '/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->get('/api/accounts/'.$password->account->id)
|
||||
->assertJson([
|
||||
'types' => [
|
||||
[
|
||||
'id' => $accountType->id,
|
||||
'key' => $accountType->key
|
||||
->get('/api/accounts/' . $password->account->id)
|
||||
->assertJson([
|
||||
'types' => [
|
||||
[
|
||||
'id' => $accountType->id,
|
||||
'key' => $accountType->key
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
]);
|
||||
|
||||
// Remove
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id)
|
||||
->delete('/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
|
||||
->assertStatus(200);
|
||||
|
||||
$this->assertEquals(0, DB::table('account_account_type')->count());
|
||||
|
||||
// Retry
|
||||
$this->keyAuthenticated($admin->account)
|
||||
->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id)
|
||||
->delete('/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
|
||||
->assertStatus(403);
|
||||
$this->assertEquals(0, DB::table('account_account_type')->count());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ abstract class TestCase extends BaseTestCase
|
|||
|
||||
const ALGORITHMS = ['md5' => 'MD5', 'sha256' => 'SHA-256'];
|
||||
|
||||
protected $route = '/api/accounts/me';
|
||||
protected $method = 'GET';
|
||||
|
||||
protected function keyAuthenticated(Account $account)
|
||||
{
|
||||
return $this->withHeaders([
|
||||
|
|
@ -37,11 +40,11 @@ abstract class TestCase extends BaseTestCase
|
|||
]);
|
||||
}
|
||||
|
||||
protected function generateFirstResponse(Password $password)
|
||||
protected function generateFirstResponse(Password $password, ?string $method = null, ?string $route = null)
|
||||
{
|
||||
return $this->withHeaders([
|
||||
'From' => 'sip:'.$password->account->identifier
|
||||
])->json($this->method, $this->route);
|
||||
])->json($method ?? $this->method, $route ?? $this->route);
|
||||
}
|
||||
|
||||
protected function generateSecondResponse(Password $password, $firstResponse)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue