Compare commits

...

24 commits

Author SHA1 Message Date
Timothée Jaussoin
17300d3ae5 Fix #117 Redeem properly the tokens to prevent reuse 2023-08-29 09:55:04 +00:00
Timothée Jaussoin
1f84042e59 Fix #116 Fix Change SMS endpoint 2023-08-24 14:35:29 +02:00
Timothée Jaussoin
3f3ddda282 Fix #115 Check if $account exists 2023-08-24 14:18:12 +02:00
Timothée Jaussoin
7562218480 Release the 1.3.1 2023-07-28 10:48:52 +02:00
Timothée Jaussoin
2ddad9d851 Fix #111 Disable phone authentication routes when the related toggle is set to false 2023-07-28 10:22:19 +02:00
Timothée Jaussoin
c102bcec77 Release the 1.3 2023-06-12 13:00:41 +00:00
Timothée Jaussoin
25bec44486 Fix #108 Add + in the sed selection when renaming packages 2023-06-12 12:31:12 +00:00
Timothée Jaussoin
6555112715 Fix #97 Validate usernames with a configurable regex 2023-05-31 15:45:45 +02:00
Timothée Jaussoin
30b8e492d8 Fix #105 Return 404 and not 403 on POST... 2023-05-29 12:47:50 +00:00
Timothée Jaussoin
ec1bdba376 Fix #95 PUT /accounts admin endpoint implementation 2023-05-25 15:15:50 +00:00
Timothée Jaussoin
ca4320e734 Fix #104 Return validation URL when creation an account creation request token 2023-05-23 17:07:57 +02:00
Timothée Jaussoin
1e5052da1d Fix #103 Limit the size of account_creation_request_tokens token column to 16 characters 2023-05-23 16:35:57 +02:00
Timothée Jaussoin
cf6b007f26 Fix #102 Implement AccountCreationRequestToken 2023-05-23 14:29:01 +00:00
Timothée Jaussoin
6ca20e6a9c Fix #92 Add two new endpoints regarding email account reset and account search per email for admins 2023-05-10 10:01:46 +00:00
Timothée Jaussoin
d2b2a9dd6d Fix #100 Fix and move the SMS log to OvhSMS library 2023-05-04 17:49:10 +02:00
Timothée Jaussoin
82b5e967dc Fix #93 Remove the .htaccess file, redundant with the existing Apache configuration 2023-05-04 16:53:22 +02:00
Timothée Jaussoin
0d21f3fda9 Fix #99 Remove username restriction in Public unsecure endpoint 2023-05-04 14:36:41 +00:00
Timothée Jaussoin
9569c79008 Fix #98 Validate the existence of a similar key on POST /account_types for 1.3 2023-05-04 13:41:08 +00:00
Timothée Jaussoin
e11d55e3f9 Fix #94 Implement the deprecated endpoint changes + tests + documentation 2023-05-02 17:38:34 +02:00
Timothée Jaussoin
3f35954071 Fix #96 Add the missing sipmessage paramater value for dtmf_protocol in the documentation Release 1.3 2023-04-27 15:27:02 +00:00
Timothée Jaussoin
0a8eda05c4 Fix #60 Rename code to confirmation_key to be more consistent with the API,... 2023-04-26 15:45:32 +00:00
Timothée Jaussoin
c22e713e1a Complete the changelog 2023-04-06 14:08:05 +00:00
Timothée Jaussoin
9cb5953209 Fix #90 Also deploy packages on release branches 2023-04-06 15:33:48 +02:00
Timothée Jaussoin
233feef9d8 Fix #89 Rename packages before deployment to prevent some deployment issues 2023-04-06 14:52:56 +02:00
57 changed files with 1063 additions and 465 deletions

View file

@ -28,11 +28,12 @@ remi-deploy:
stage: deploy stage: deploy
tags: ["docker"] tags: ["docker"]
only: only:
refs: - master
- master - /^release/.*$/
before_script: 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) - eval $(ssh-agent -s)
- echo "$DEPLOY_USER_KEY" | tr -d '\r' | ssh-add - > /dev/null - echo "$DEPLOY_USER_KEY" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh && chmod 700 ~/.ssh - mkdir -p ~/.ssh && chmod 700 ~/.ssh

View file

@ -3,7 +3,7 @@ variables:
DEBIAN_11_IMAGE_VERSION: 20230322_172926_missing_tools DEBIAN_11_IMAGE_VERSION: 20230322_172926_missing_tools
PHP_REDIS_REMI_VERSION: php-pecl-redis5-5.3.6-1 PHP_REDIS_REMI_VERSION: php-pecl-redis5-5.3.6-1
PHP_IGBINARY_REMI_VERSION: php-pecl-igbinary-3.2.14-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 PHP_XMLRPC_REMI_VERSION: php-pecl-xmlrpc-1.0.0~rc3-2
include: include:

View file

@ -1,7 +1,12 @@
# Flexisip Account Manager Changelog # Flexisip Account Manager Changelog
v1.3.1
------
- Fix #111 Disable phone authentication routes when the related toggle is set to false
v1.3 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 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 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 - 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 #79 Add a refresh_password parameter to the provisioning URLs
- Fix #78 Add a APP_ACCOUNTS_EMAIL_UNIQUE environnement setting - Fix #78 Add a APP_ACCOUNTS_EMAIL_UNIQUE environnement setting
- Fix #30 Remove APP_EVERYONE_IS_ADMIN - 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 v1.2
---- ----

View file

@ -24,6 +24,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_EMAIL_UNIQUE=false # Emails are unique between all the accounts
ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE=false ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE=false
ACCOUNT_BLACKLISTED_USERNAMES= ACCOUNT_BLACKLISTED_USERNAMES=
ACCOUNT_USERNAME_REGEX="^[a-z0-9+_.-]*$"
# Account provisioning # Account provisioning
ACCOUNT_PROVISIONING_RC_FILE= ACCOUNT_PROVISIONING_RC_FILE=

View file

@ -32,6 +32,7 @@ use App\Password;
use App\EmailChanged; use App\EmailChanged;
use App\Mail\ChangingEmail; use App\Mail\ChangingEmail;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request;
class Account extends Authenticatable class Account extends Authenticatable
{ {
@ -108,62 +109,62 @@ class Account extends Authenticatable
public function activationExpiration() public function activationExpiration()
{ {
return $this->hasOne('App\ActivationExpiration'); return $this->hasOne(ActivationExpiration::class);
} }
public function admin() public function admin()
{ {
return $this->hasOne('App\Admin'); return $this->hasOne(Admin::class);
} }
public function alias() public function alias()
{ {
return $this->hasOne('App\Alias'); return $this->hasOne(Alias::class);
} }
public function apiKey() public function apiKey()
{ {
return $this->hasOne('App\ApiKey'); return $this->hasOne(ApiKey::class);
} }
public function externalAccount() public function externalAccount()
{ {
return $this->hasOne('App\ExternalAccount'); return $this->hasOne(ExternalAccount::class);
} }
public function contacts() 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() public function emailChanged()
{ {
return $this->hasOne('App\EmailChanged'); return $this->hasOne(EmailChanged::class);
} }
public function nonces() public function nonces()
{ {
return $this->hasMany('App\DigestNonce'); return $this->hasMany(DigestNonce::class);
} }
public function authTokens() public function authTokens()
{ {
return $this->hasMany('App\AuthToken'); return $this->hasMany(AuthToken::class);
} }
public function passwords() public function passwords()
{ {
return $this->hasMany('App\Password'); return $this->hasMany(Password::class);
} }
public function phoneChangeCode() public function phoneChangeCode()
{ {
return $this->hasOne('App\PhoneChangeCode'); return $this->hasOne(PhoneChangeCode::class);
} }
public function types() public function types()
{ {
return $this->belongsToMany('App\AccountType'); return $this->belongsToMany(AccountType::class);
} }
/** /**
@ -202,6 +203,19 @@ class Account extends Authenticatable
return null; 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() public function getConfirmationKeyExpiresAttribute()
{ {
if ($this->activationExpiration) { if ($this->activationExpiration) {
@ -302,9 +316,20 @@ class Account extends Authenticatable
return $this->provisioning_token; 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() public function hasTombstone()
@ -325,6 +350,14 @@ class Account extends Authenticatable
$password->save(); $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() public function toVcard4()
{ {
$vcard = 'BEGIN:VCARD $vcard = 'BEGIN:VCARD

View file

@ -30,6 +30,6 @@ class AccountAction extends Model
public function account() public function account()
{ {
return $this->belongsTo('App\Account'); return $this->belongsTo(Account::class);
} }
} }

View 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;
}
}

View file

@ -25,4 +25,11 @@ use Illuminate\Database\Eloquent\Model;
class AccountCreationToken extends Model class AccountCreationToken extends Model
{ {
use HasFactory; use HasFactory;
protected $hidden = ['id', 'updated_at', 'created_at'];
public function accountCreationRequestToken()
{
return $this->hasOne(AccountCreationRequestToken::class, 'acc_creation_token_id');
}
} }

View file

@ -30,6 +30,6 @@ class AccountType extends Model
public function accounts() public function accounts()
{ {
return $this->belongsToMany('App\Account'); return $this->belongsToMany(Account::class);
} }
} }

View file

@ -33,7 +33,7 @@ class ActivationExpiration extends Model
public function account() public function account()
{ {
return $this->belongsTo('App\Account'); return $this->belongsTo(Account::class);
} }
public function isExpired() public function isExpired()

View file

@ -30,4 +30,16 @@ class Alias extends Model
{ {
return $this->belongsTo('App\Account'); 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);
}
} }

View file

@ -22,6 +22,7 @@ use Illuminate\Support\Str;
use App\Account; use App\Account;
use App\DigestNonce; use App\DigestNonce;
use App\ExternalAccount; use App\ExternalAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension; use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
@ -119,3 +120,13 @@ function isRegularExpression($string): bool
return $isRegularExpression; 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');
}

View file

@ -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');
}
}

View file

@ -144,7 +144,7 @@ class ProvisioningController extends Controller
} }
// Password reset // Password reset
if ($request->has('reset_password')) { if ($account && $request->has('reset_password')) {
$account->updatePassword(Str::random(10)); $account->updatePassword(Str::random(10));
} }

View file

@ -21,7 +21,6 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Carbon\Carbon; use Carbon\Carbon;
@ -31,13 +30,6 @@ use App\Alias;
use App\ExternalAccount; use App\ExternalAccount;
use App\Http\Requests\CreateAccountRequest; use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest; 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 class AccountController extends Controller
{ {
@ -73,45 +65,19 @@ class AccountController extends Controller
public function store(CreateAccountRequest $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',
],
'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 = new Account;
$account->username = $request->get('username'); $account->username = $request->get('username');
$account->email = $request->get('email'); $account->email = $request->get('email');
$account->display_name = $request->get('display_name'); $account->display_name = $request->get('display_name');
$account->domain = $this->resolveDomain($request); $account->domain = resolveDomain($request);
$account->ip_address = $request->ip(); $account->ip_address = $request->ip();
$account->creation_time = Carbon::now(); $account->creation_time = Carbon::now();
$account->user_agent = config('app.name'); $account->user_agent = config('app.name');
$account->dtmf_protocol = $request->get('dtmf_protocol'); $account->dtmf_protocol = $request->get('dtmf_protocol');
$account->save(); $account->save();
$this->fillPassword($request, $account); $account->phone = $request->get('phone');
$this->fillPhone($request, $account); $account->fillPassword($request);
Log::channel('events')->info('Web Admin: Account created', ['id' => $account->identifier]); 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) 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 = Account::findOrFail($id);
$account->username = $request->get('username'); $account->username = $request->get('username');
$account->email = $request->get('email'); $account->email = $request->get('email');
@ -161,8 +101,8 @@ class AccountController extends Controller
$account->dtmf_protocol = $request->get('dtmf_protocol'); $account->dtmf_protocol = $request->get('dtmf_protocol');
$account->save(); $account->save();
$this->fillPassword($request, $account); $account->phone = $request->get('phone');
$this->fillPhone($request, $account); $account->fillPassword($request);
Log::channel('events')->info('Web Admin: Account updated', ['id' => $account->identifier]); 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'); 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();
}
}
} }

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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\Http\Request;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -28,12 +28,13 @@ use App\Http\Controllers\Controller;
use Carbon\Carbon; use Carbon\Carbon;
use App\Account; use App\Account;
use App\AccountTombstone;
use App\AccountCreationToken; use App\AccountCreationToken;
use App\AccountTombstone;
use App\Alias; use App\Alias;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Libraries\OvhSMS; use App\Libraries\OvhSMS;
use App\Mail\RegisterConfirmation; use App\Mail\RegisterConfirmation;
use App\Rules\AccountCreationToken as RulesAccountCreationToken;
use App\Rules\BlacklistedUsername; use App\Rules\BlacklistedUsername;
use App\Rules\IsNotPhoneNumber; use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase; use App\Rules\NoUppercase;
@ -70,11 +71,13 @@ class AccountController extends Controller
$alias = Alias::where('alias', $phone)->first(); $alias = Alias::where('alias', $phone)->first();
$account = $alias $account = $alias
? $alias->account ? $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([ return \response()->json([
'activated' => $account->activated, 'activated' => $account->activated,
'realm' => $account->realm 'realm' => $account->realm,
'phone' => (bool)$alias
]); ]);
} }
@ -88,9 +91,8 @@ class AccountController extends Controller
$request->validate([ $request->validate([
'username' => [ 'username' => [
'prohibits:phone', 'required_without:phone',
new NoUppercase, new NoUppercase,
new IsNotPhoneNumber,
new BlacklistedUsername, new BlacklistedUsername,
new SIPUsername, new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) { Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
@ -109,10 +111,14 @@ class AccountController extends Controller
: 'required_without:phone|email', : 'required_without:phone|email',
'phone' => [ 'phone' => [
'required_without:email', 'required_without:email',
'prohibits:username', 'required_without:username',
'unique:aliases,alias', 'unique:aliases,alias',
'unique:accounts,username', 'unique:accounts,username',
new WithoutSpaces, 'starts_with:+' new WithoutSpaces, 'starts_with:+'
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken
] ]
]); ]);
@ -127,12 +133,18 @@ class AccountController extends Controller
: config('app.sip_domain'); : config('app.sip_domain');
$account->ip_address = $request->ip(); $account->ip_address = $request->ip();
$account->creation_time = Carbon::now(); $account->creation_time = Carbon::now();
$account->user_agent = config('app.name'); $account->user_agent = $request->header('User-Agent') ?? config('app.name');
$account->provision(); $account->provision();
$account->save(); $account->save();
$account->updatePassword($request->get('password'), $request->get('algorithm')); $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]); Log::channel('events')->info('API: Account created using the public endpoint', ['id' => $account->identifier]);
// Send validation by phone // Send validation by phone
@ -178,17 +190,27 @@ class AccountController extends Controller
$request->validate([ $request->validate([
'phone' => [ 'phone' => [
'required', new WithoutSpaces, 'starts_with:+' 'required', new WithoutSpaces, 'starts_with:+'
],
'account_creation_token' => [
'required',
new RulesAccountCreationToken
] ]
]); ]);
$alias = Alias::where('alias', $request->get('phone'))->first(); $alias = Alias::where('alias', $request->get('phone'))->first();
$account = $alias $account = $alias
? $alias->account ? $alias->account
: Account::sip($request->get('phone'))->firstOrFail(); : Account::sip($request->get('phone') . '@' . config('app.sip_domain'))->firstOrFail();
$account->confirmation_key = generatePin(); $account->confirmation_key = generatePin();
$account->save(); $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]); Log::channel('events')->info('API: Account recovery by phone', ['id' => $account->identifier]);
$ovhSMS = new OvhSMS; $ovhSMS = new OvhSMS;
@ -202,9 +224,12 @@ class AccountController extends Controller
{ {
if (!config('app.dangerous_endpoints')) return abort(404); if (!config('app.dangerous_endpoints')) return abort(404);
$account = Account::sip($sip) $alias = Alias::sip($sip)->first();
->where('confirmation_key', $recoveryKey) $account = $alias
->firstOrFail(); ? $alias->account
: Account::sip($sip)->firstOrFail();
if ($account->confirmation_key != $recoveryKey) abort(404);
if ($account->activationExpired()) abort(403, 'Activation expired'); if ($account->activationExpired()) abort(403, 'Activation expired');
@ -240,29 +265,14 @@ class AccountController extends Controller
'password' => 'required|filled', 'password' => 'required|filled',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(), 'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'account_creation_token' => [ 'account_creation_token' => [
'required_without:token', 'required',
Rule::exists('account_creation_tokens', 'token')->where(function ($query) { new RulesAccountCreationToken
$query->where('used', false);
}),
'size:' . WebAuthenticateController::$emailCodeSize
], ],
'email' => config('app.account_email_unique') 'email' => config('app.account_email_unique')
? 'nullable|email|unique:accounts,email' ? 'nullable|email|unique:accounts,email'
: 'nullable|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 = new Account;
$account->username = $request->get('username'); $account->username = $request->get('username');
$account->email = $request->get('email'); $account->email = $request->get('email');
@ -277,6 +287,11 @@ class AccountController extends Controller
$account->updatePassword($request->get('password'), $request->get('algorithm')); $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]); Log::channel('events')->info('API: Account created', ['id' => $account->identifier]);
// Full reload // Full reload
@ -285,12 +300,17 @@ class AccountController extends Controller
public function activateEmail(Request $request, string $sip) public function activateEmail(Request $request, string $sip)
{ {
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([ $request->validate([
'code' => 'required|size:' . WebAuthenticateController::$emailCodeSize 'confirmation_key' => 'required|size:' . WebAuthenticateController::$emailCodeSize
]); ]);
$account = Account::sip($sip) $account = Account::sip($sip)
->where('confirmation_key', $request->get('code')) ->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail(); ->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired'); if ($account->activationExpired()) abort(403, 'Activation expired');
@ -306,12 +326,17 @@ class AccountController extends Controller
public function activatePhone(Request $request, string $sip) public function activatePhone(Request $request, string $sip)
{ {
// For retro-compatibility
if ($request->has('code')) {
$request->merge(['confirmation_key' => $request->get('code')]);
}
$request->validate([ $request->validate([
'code' => 'required|digits:4' 'confirmation_key' => 'required|digits:4'
]); ]);
$account = Account::sip($sip) $account = Account::sip($sip)
->where('confirmation_key', $request->get('code')) ->where('confirmation_key', $request->get('confirmation_key'))
->firstOrFail(); ->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired'); if ($account->activationExpired()) abort(403, 'Activation expired');

View file

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api\Account;
use App\AuthToken; use App\AuthToken;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api\Account;
use App\AuthToken; use App\AuthToken;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class AccountContactController extends Controller class ContactController extends Controller
{ {
private $selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol']; private $selected = ['id', 'username', 'domain', 'activated', 'dtmf_protocol'];

View file

@ -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;
}
}

View file

@ -17,8 +17,9 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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\AccountCreationRequestToken;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -27,8 +28,9 @@ use Illuminate\Support\Facades\Log;
use App\AccountCreationToken; use App\AccountCreationToken;
use App\Libraries\FlexisipPusherConnector; use App\Libraries\FlexisipPusherConnector;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Rules\AccountCreationRequestToken as RulesAccountCreationRequestToken;
class AccountCreationTokenController extends Controller class CreationTokenController extends Controller
{ {
public function sendByPush(Request $request) public function sendByPush(Request $request)
{ {
@ -55,4 +57,32 @@ class AccountCreationTokenController extends Controller
abort(503, "Token not sent"); 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);
}
} }

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -29,7 +29,7 @@ use App\Libraries\OvhSMS;
use App\PhoneChangeCode; use App\PhoneChangeCode;
use App\Alias; use App\Alias;
class AccountPhoneController extends Controller class PhoneController extends Controller
{ {
public function requestUpdate(Request $request) public function requestUpdate(Request $request)
{ {

View file

@ -22,7 +22,6 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Carbon\Carbon; use Carbon\Carbon;
@ -33,11 +32,10 @@ use App\ActivationExpiration;
use App\Admin; use App\Admin;
use App\Alias; use App\Alias;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Rules\BlacklistedUsername; use App\Http\Requests\CreateAccountRequest;
use App\Rules\IsNotPhoneNumber; use App\Http\Requests\UpdateAccountRequest;
use App\Rules\NoUppercase; use App\Mail\PasswordAuthentication;
use App\Rules\SIPUsername; use Illuminate\Support\Facades\Mail;
use App\Rules\WithoutSpaces;
class AccountController extends Controller class AccountController extends Controller
{ {
@ -56,6 +54,11 @@ class AccountController extends Controller
return Account::sip($sip)->firstOrFail(); return Account::sip($sip)->firstOrFail();
} }
public function searchByEmail(Request $request, string $email)
{
return Account::where('email', $email)->firstOrFail();
}
public function destroy($id) public function destroy($id)
{ {
$account = Account::findOrFail($id); $account = Account::findOrFail($id);
@ -105,36 +108,15 @@ class AccountController extends Controller
return $account->makeVisible(['provisioning_token']); return $account->makeVisible(['provisioning_token']);
} }
public function store(Request $request) public function store(CreateAccountRequest $request)
{ {
$request->validate([ $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', 'algorithm' => 'required|in:SHA-256,MD5',
'password' => 'required|filled',
'admin' => 'boolean|nullable', 'admin' => 'boolean|nullable',
'activated' => 'boolean|nullable', 'activated' => 'boolean|nullable',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'confirmation_key_expires' => [ 'confirmation_key_expires' => [
'date_format:Y-m-d H:i:s', 'date_format:Y-m-d H:i:s',
'nullable', '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->ip_address = $request->ip();
$account->dtmf_protocol = $request->get('dtmf_protocol'); $account->dtmf_protocol = $request->get('dtmf_protocol');
$account->creation_time = Carbon::now(); $account->creation_time = Carbon::now();
$account->domain = $this->resolveDomain($request); $account->domain = resolveDomain($request);
$account->user_agent = $request->header('User-Agent') ?? config('app.name'); $account->user_agent = $request->header('User-Agent') ?? config('app.name');
if (!$request->has('activated') || !(bool)$request->get('activated')) { if (!$request->has('activated') || !(bool)$request->get('activated')) {
@ -165,27 +147,45 @@ class AccountController extends Controller
} }
$account->updatePassword($request->get('password'), $request->get('algorithm')); $account->updatePassword($request->get('password'), $request->get('algorithm'));
$account->admin = $request->has('admin') && (bool)$request->get('admin');
if ($request->has('admin') && (bool)$request->get('admin')) { $account->phone = $request->get('phone');
$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();
}
// Full reload // Full reload
$account = Account::withoutGlobalScopes()->find($account->id); $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) public function typeAdd(int $id, int $typeId)
@ -207,4 +207,18 @@ class AccountController extends Controller
return Account::findOrFail($id)->types()->detach($typeId); 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']);
}
} }

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View file

@ -41,7 +41,7 @@ class AccountTypeController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$request->validate([ $request->validate([
'key' => ['required', 'alpha_dash', new NoUppercase], 'key' => ['required', 'alpha_dash', new NoUppercase, 'unique:account_types,key'],
]); ]);
$accountType = new AccountType; $accountType = new AccountType;

View file

@ -1,6 +1,6 @@
<?php <?php
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;

View file

@ -11,14 +11,4 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, DispatchesJobs, ValidatesRequests; 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');
}
} }

View file

@ -19,7 +19,7 @@ class AuthenticateAdmin
return redirect()->route('account.login'); return redirect()->route('account.login');
} }
if (!$request->user()->isAdmin()) { if (!$request->user()->admin) {
return abort(403, 'Unauthorized area'); return abort(403, 'Unauthorized area');
} }

View file

@ -29,13 +29,14 @@ class CreateAccountRequest extends FormRequest
new BlacklistedUsername, new BlacklistedUsername,
new SIPUsername, new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) { Rule::unique('accounts', 'username')->where(function ($query) {
$query->where('domain', config('app.sip_domain')); $query->where('domain', resolveDomain($this));
}), }),
'filled', 'filled',
], ],
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '',
'password' => 'required|min:3', '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(), 'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'phone' => [ 'phone' => [
'nullable', 'nullable',

View file

@ -33,8 +33,11 @@ class UpdateAccountRequest extends FormRequest
})->ignore($this->route('id'), 'id'), })->ignore($this->route('id'), 'id'),
'filled', 'filled',
], ],
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '', 'email' => [
'email' => 'nullable|email', 'nullable',
'email',
config('app.account_email_unique') ? Rule::unique('accounts', 'email')->ignore($this->route('id')) : null
],
'password_sha256' => 'nullable|min:3', 'password_sha256' => 'nullable|min:3',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(), 'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'phone' => [ 'phone' => [

View file

@ -41,7 +41,7 @@ class OvhSMS
); );
try { try {
$smsServices = $this->api->get('/sms/'); $smsServices = $this->api->get('/sms');
if (!empty($smsServices)) { if (!empty($smsServices)) {
$this->smsService = $smsServices[0]; $this->smsService = $smsServices[0];
@ -71,6 +71,8 @@ class OvhSMS
'validityPeriod' => 2880 'validityPeriod' => 2880
]; ];
Log::channel('events')->info('OVH SMS sending', ['to' => $to, 'message' => $message]);
try { try {
$this->api->post('/sms/'. $this->smsService . '/jobs', $content); $this->api->post('/sms/'. $this->smsService . '/jobs', $content);
// One credit removed // One credit removed

View 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';
}
}

View 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';
}
}

View file

@ -1,4 +1,21 @@
<?php <?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; namespace App\Rules;

View file

@ -26,7 +26,7 @@ class SIPUsername implements Rule
{ {
public function passes($attribute, $value) 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() public function message()

View file

@ -31,6 +31,7 @@ return [
'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false), 'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false),
'consume_external_account_on_create' => env('ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE', false), 'consume_external_account_on_create' => env('ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE', false),
'blacklisted_usernames' => env('ACCOUNT_BLACKLISTED_USERNAMES', ''), '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 * Time limit before the API Key and related cookie are expired

View file

@ -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');
});
}
}

View file

@ -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();
});
}
}

View file

@ -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');
}
}

View file

@ -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>

View file

@ -29,6 +29,10 @@ body > div {
max-width: 800px; max-width: 800px;
} }
.container.large {
max-width: 1024px;
}
body > footer::before { body > footer::before {
background-color: white; background-color: white;
background-position: bottom center; background-position: bottom center;

View file

@ -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

View file

@ -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

View file

@ -45,7 +45,7 @@
</a> </a>
</div> </div>
@if($account->isAdmin()) @if($account->admin)
<h3>Admin area</h3> <h3>Admin area</h3>
<div class="list-group mb-3"> <div class="list-group mb-3">
<a href="{{ route('admin.account.index') }}" class="list-group-item list-group-item-action"> <a href="{{ route('admin.account.index') }}" class="list-group-item list-group-item-action">

View file

@ -1,4 +1,4 @@
@extends('layouts.main') @extends('layouts.main', ['large' => true])
@section('content') @section('content')
{{-- This view is only a wrapper for the markdown page --}} {{-- This view is only a wrapper for the markdown page --}}

View file

@ -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> <span class="badge badge-success">Public</span>
Returns `pong` 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 ## 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` ### `POST /account_creation_tokens/send-by-push`
<span class="badge badge-success">Public</span> <span class="badge badge-success">Public</span>
@ -94,6 +104,16 @@ JSON parameters:
* `pn_param` the push notification parameter * `pn_param` the push notification parameter
* `pn_prid` the push notification unique id * `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` ### `POST /account_creation_tokens`
<span class="badge badge-warning">Admin</span> <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 * `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` * `email` optional if `phone` set, an email, set an email to the account, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `phone` required if `username` not set, optional if `email` set, a phone number, set a phone number to the account * `phone` required if `username` not set, optional if `email` set, a phone number, set a phone number to the account
* `account_creation_token` the unique `account_creation_token`
### `POST /accounts/with-account-creation-token` ### `POST /accounts/with-account-creation-token`
<span class="badge badge-success">Public</span> <span class="badge badge-success">Public</span>
@ -146,7 +167,7 @@ JSON parameters:
* `password` required minimum 6 characters * `password` required minimum 6 characters
* `algorithm` required, values can be `SHA-256` or `MD5` * `algorithm` required, values can be `SHA-256` or `MD5`
* `account_creation_token` the unique `account_creation_token` * `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` ### `GET /accounts/{sip}/info`
<span class="badge badge-success">Public</span> <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. Retrieve public information about the account.
Return `404` if the account doesn't exists. Return `404` if the account doesn't exists.
Return `phone: true` if the returned account has a phone number.
### `POST /accounts/recover-by-phone` ### `POST /accounts/recover-by-phone`
@if(config('app.dangerous_endpoints'))**Enabled on this instance**@else**Not enabled on this instance**@endif @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: JSON parameters:
* `phone` required the phone number to send the SMS to * `phone` required the phone number to send the SMS to
* `account_creation_token` the unique `account_creation_token`
### `GET /accounts/{sip}/recover/{recover_key}` ### `GET /accounts/{sip}/recover/{recover_key}`
@if(config('app.dangerous_endpoints'))**Enabled on this instance**@else**Not enabled on this instance**@endif @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-success">Public</span>
<span class="badge badge-warning">Unsecure endpoint</span> <span class="badge badge-warning">Unsecure endpoint</span>
Activate the account if the correct `recover_key` is provided. 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 the account information (including the hashed password) if valid.
Return `404` if the account doesn't exists. 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. Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
JSON parameters: JSON parameters:
* `code` the code * `confirmation_key` the confirmation key
### `POST /accounts/{sip}/activate/phone` ### `POST /accounts/{sip}/activate/phone`
<span class="badge badge-success">Public</span> <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. Return `404` if the account doesn't exists or if the code is incorrect, the validated account otherwise.
JSON parameters: JSON parameters:
* `code` the PIN code * `confirmation_key` the PIN code
### `GET /accounts/me/api_key/{auth_token}` ### `GET /accounts/me/api_key/{auth_token}`
<span class="badge badge-success">Public</span> <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` * `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 * `admin` optional, a boolean, set to `false` by default, create an admin account
* `phone` optional, a phone number, set a phone number to the account * `phone` optional, a 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`. * `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` ### `GET /accounts`
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Retrieve all the accounts, paginated. Retrieve all the accounts, paginated.
@ -272,10 +314,18 @@ Retrieve all the accounts, paginated.
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Retrieve a specific account. 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` ### `GET /accounts/{sip}/search`
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Search for a specific account by sip address. 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}` ### `DELETE /accounts/{id}`
<span class="badge badge-warning">Admin</span> <span class="badge badge-warning">Admin</span>
Delete a specific account and its related information. Delete a specific account and its related information.

View file

@ -33,7 +33,7 @@
@endsection @endsection
@section('body') @section('body')
<div class="container pt-4"> <div class="container @if (isset($large) && $large) large @endif pt-4">
@include('parts.errors') @include('parts.errors')
@yield('content') @yield('content')
</div> </div>

View file

@ -26,69 +26,74 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
}); });
Route::get('ping', 'Api\PingController@ping'); 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'); Route::get('accounts/{sip}/info', 'Api\Account\AccountController@info');
// Old URL, for retro-compatibility
Route::post('accounts/with-token', 'Api\AccountController@store');
Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmail'); Route::post('accounts/{sip}/activate/email', 'Api\Account\AccountController@activateEmail');
Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone'); Route::post('accounts/{sip}/activate/phone', 'Api\Account\AccountController@activatePhone');
// /!\ Dangerous endpoints // /!\ Dangerous endpoints
Route::post('accounts/public', 'Api\AccountController@storePublic'); Route::post('accounts/public', 'Api\Account\AccountController@storePublic');
Route::get('accounts/{sip}/recover/{recovery_key}', 'Api\AccountController@recoverUsingKey'); Route::get('accounts/{sip}/recover/{recovery_key}', 'Api\Account\AccountController@recoverUsingKey');
Route::post('accounts/recover-by-phone', 'Api\AccountController@recoverByPhone'); Route::post('accounts/recover-by-phone', 'Api\Account\AccountController@recoverByPhone');
Route::get('accounts/{phone}/info-by-phone', 'Api\AccountController@phoneInfo'); 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::group(['middleware' => ['auth.digest_or_key']], function () {
Route::get('statistic/month', 'Api\StatisticController@month'); Route::get('statistic/month', 'Api\StatisticController@month');
Route::get('statistic/week', 'Api\StatisticController@week'); Route::get('statistic/week', 'Api\StatisticController@week');
Route::get('statistic/day', 'Api\StatisticController@day'); 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::get('accounts/me', 'Api\Account\AccountController@show');
Route::delete('accounts/me', 'Api\AccountController@delete'); Route::delete('accounts/me', 'Api\Account\AccountController@delete');
Route::get('accounts/me/provision', 'Api\AccountController@provision'); Route::get('accounts/me/provision', 'Api\Account\AccountController@provision');
Route::post('accounts/me/phone/request', 'Api\AccountPhoneController@requestUpdate'); Route::post('accounts/me/phone/request', 'Api\Account\PhoneController@requestUpdate');
Route::post('accounts/me/phone', 'Api\AccountPhoneController@update'); Route::post('accounts/me/phone', 'Api\Account\PhoneController@update');
Route::get('accounts/me/devices', 'Api\DeviceController@index'); Route::get('accounts/me/devices', 'Api\Account\DeviceController@index');
Route::delete('accounts/me/devices/{uuid}', 'Api\DeviceController@destroy'); Route::delete('accounts/me/devices/{uuid}', 'Api\Account\DeviceController@destroy');
Route::post('accounts/me/email/request', 'Api\EmailController@requestUpdate'); Route::post('accounts/me/email/request', 'Api\Account\EmailController@requestUpdate');
Route::post('accounts/me/password', 'Api\PasswordController@update'); Route::post('accounts/me/password', 'Api\Account\PasswordController@update');
Route::get('accounts/me/contacts/{sip}', 'Api\AccountContactController@show'); Route::get('accounts/me/contacts/{sip}', 'Api\Account\ContactController@show');
Route::get('accounts/me/contacts', 'Api\AccountContactController@index'); Route::get('accounts/me/contacts', 'Api\Account\ContactController@index');
Route::group(['middleware' => ['auth.admin']], function () { Route::group(['middleware' => ['auth.admin']], function () {
if (!empty(config('app.linphone_daemon_unix_pipe'))) { 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 // Accounts
Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate'); Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate');
Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate');
Route::get('accounts/{id}/provision', 'Api\Admin\AccountController@provision'); 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::post('accounts', 'Api\Admin\AccountController@store');
Route::put('accounts/{id}', 'Api\Admin\AccountController@update');
Route::get('accounts', 'Api\Admin\AccountController@index'); 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::get('accounts/{id}', 'Api\Admin\AccountController@show');
Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy'); 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 // Account actions
Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index'); Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index');

View file

@ -29,13 +29,18 @@ if (config('app.web_panel')) {
Route::get('authenticate/email/check/{sip}', 'Account\AuthenticateController@checkEmail')->name('account.check.email'); 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('authenticate/email/{code}', 'Account\AuthenticateController@validateEmail')->name('account.authenticate.email_confirm');
Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone'); if (config('app.phone_authentication')) {
Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone'); Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('account.login_phone');
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm'); 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('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::group(['middleware' => 'auth.digest_or_key'], function () {
Route::get('provisioning/me', 'Account\ProvisioningController@me')->name('provisioning.me'); Route::get('provisioning/me', 'Account\ProvisioningController@me')->name('provisioning.me');

View file

@ -188,9 +188,9 @@ class AccountProvisioningTest extends TestCase
->assertStatus(201) ->assertStatus(201)
->assertJson([ ->assertJson([
'token' => true 'token' => true
])->content(); ]);
$authToken = json_decode($response)->token; $authToken = $response->json('token');
$password = Password::factory()->create(); $password = Password::factory()->create();
$password->account->generateApiKey(); $password->account->generateApiKey();

View file

@ -64,9 +64,9 @@ class ApiAccountApiKeyTest extends TestCase
->assertStatus(201) ->assertStatus(201)
->assertJson([ ->assertJson([
'token' => true 'token' => true
])->content(); ]);
$authToken = json_decode($response)->token; $authToken = $response->json('token');
// Try to retrieve an API key from the un-attached auth_token // Try to retrieve an API key from the un-attached auth_token
$response = $this->json($this->method, $this->route . '/' . $authToken) $response = $this->json($this->method, $this->route . '/' . $authToken)
@ -95,9 +95,9 @@ class ApiAccountApiKeyTest extends TestCase
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'api_key' => true 'api_key' => true
])->content(); ]);
$apiKey = json_decode($response)->api_key; $apiKey = $response->json('api_key');
// Re-retrieve // Re-retrieve
$this->json($this->method, $this->route . '/' . $authToken) $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 // Check the if the API key can be used for the account
$response = $this->withHeaders(['x-api-key' => $apiKey]) $response = $this->withHeaders(['x-api-key' => $apiKey])
->json($this->method, '/api/accounts/me') ->json($this->method, '/api/accounts/me')
->assertStatus(200) ->assertStatus(200);
->content();
// Try with a wrong From // Try with a wrong From
$response = $this->withHeaders([ $response = $this->withHeaders([
@ -115,10 +114,9 @@ class ApiAccountApiKeyTest extends TestCase
'From' => 'sip:baduser@server.tld' 'From' => 'sip:baduser@server.tld'
]) ])
->json($this->method, '/api/accounts/me') ->json($this->method, '/api/accounts/me')
->assertStatus(200) ->assertStatus(200);
->content();
// Check if the account was correctly attached // Check if the account was correctly attached
$this->assertEquals(json_decode($response)->email, $password->account->email); $this->assertEquals($response->json('email'), $password->account->email);
} }
} }

View file

@ -19,17 +19,24 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Account;
use App\AccountCreationRequestToken;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
use App\AccountCreationToken; use App\AccountCreationToken;
use App\Admin;
use Carbon\Carbon;
class ApiAccountCreationTokenTest extends TestCase class ApiAccountCreationTokenTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
protected $tokenRoute = '/api/account_creation_tokens/send-by-push'; 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 $accountRoute = '/api/accounts/with-account-creation-token';
protected $adminRoute = '/api/account_creation_tokens';
protected $method = 'POST'; protected $method = 'POST';
protected $pnProvider = 'provider'; protected $pnProvider = 'provider';
@ -52,51 +59,24 @@ class ApiAccountCreationTokenTest extends TestCase
$response->assertStatus(503); $response->assertStatus(503);
} }
/** public function testAdminEndpoint()
* For retro-compatibility only
*/
public function testRetrocopatibilityToken()
{ {
$token = AccountCreationToken::factory()->create(); $admin = Admin::factory()->create();
$admin->account->generateApiKey();
$response = $this->json($this->method, '/api/tokens', [ $response = $this->keyAuthenticated($admin->account)
'pn_provider' => $token->pn_provider, ->json($this->method, $this->adminRoute)
'pn_param' => $token->pn_param, ->assertStatus(201);
'pn_prid' => $token->pn_prid
$this->assertDatabaseHas('account_creation_tokens', [
'token' => $response->json()['token']
]); ]);
$response->assertStatus(503);
} }
public function testInvalidToken() public function testInvalidToken()
{ {
$token = AccountCreationToken::factory()->create(); $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 // Invalid token
$response = $this->json($this->method, $this->accountRoute, [ $response = $this->json($this->method, $this->accountRoute, [
'username' => 'username', 'username' => 'username',
@ -123,11 +103,13 @@ class ApiAccountCreationTokenTest extends TestCase
'account_creation_token' => $token->token 'account_creation_token' => $token->token
]); ]);
$response->assertStatus(422); $response->assertStatus(422);
$this->assertDatabaseHas('account_creation_tokens', [
'used' => true,
'account_id' => Account::where('username', 'username')->first()->id,
]);
} }
/**
* Test username blacklist
*/
public function testBlacklistedUsername() public function testBlacklistedUsername()
{ {
$token = AccountCreationToken::factory()->create(); $token = AccountCreationToken::factory()->create();
@ -165,4 +147,36 @@ class ApiAccountCreationTokenTest extends TestCase
$response->assertStatus(200); $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
);
}
} }

View file

@ -21,6 +21,7 @@ namespace Tests\Feature;
use App\Password; use App\Password;
use App\Account; use App\Account;
use App\AccountCreationToken;
use App\AccountTombstone; use App\AccountTombstone;
use App\ActivationExpiration; use App\ActivationExpiration;
use App\Admin; use App\Admin;
@ -49,7 +50,7 @@ class ApiAccountTest extends TestCase
$password = Password::factory()->create(); $password = Password::factory()->create();
$response0 = $this->generateFirstResponse($password); $response0 = $this->generateFirstResponse($password);
$response1 = $this->generateSecondResponse($password, $response0) $response1 = $this->generateSecondResponse($password, $response0)
->json($this->method, $this->route); ->json($this->method, $this->route);
$response1->assertStatus(403); $response1->assertStatus(403);
} }
@ -90,12 +91,12 @@ class ApiAccountTest extends TestCase
$domain = 'example.com'; $domain = 'example.com';
$response = $this->keyAuthenticated($password->account) $response = $this->keyAuthenticated($password->account)
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'domain' => $domain, 'domain' => $domain,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '123456', 'password' => '123456',
]); ]);
$response->assertJsonValidationErrors(['username']); $response->assertJsonValidationErrors(['username']);
} }
@ -110,28 +111,35 @@ class ApiAccountTest extends TestCase
$username = 'blabla🔥'; $username = 'blabla🔥';
$domain = 'example.com'; $domain = 'example.com';
$response = $this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'domain' => $domain, 'domain' => $domain,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '123456', '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'; $username = 'blabla hop';
$domain = 'example.com'; $domain = 'example.com';
$response = $this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'domain' => $domain, 'domain' => $domain,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '123456', 'password' => '123456',
]); ])->assertJsonValidationErrors(['username']);
$response->assertJsonValidationErrors(['username']);
} }
public function testDomain() public function testDomain()
@ -318,7 +326,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => 'blabla',
'admin' => true, 'admin' => true,
]); ]);
@ -348,7 +356,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => 'blabla',
'activated' => true, 'activated' => true,
]); ]);
@ -378,7 +386,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => 'blabla',
'activated' => false, 'activated' => false,
]); ]);
@ -407,7 +415,7 @@ class ApiAccountTest extends TestCase
/** /**
* Public information * Public information
*/ */
$this->get($this->route.'/'.$password->account->identifier.'/info') $this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false, 'activated' => false,
@ -421,7 +429,7 @@ class ApiAccountTest extends TestCase
* Retrieve the authenticated account * Retrieve the authenticated account
*/ */
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->get($this->route.'/me') ->get($this->route . '/me')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'username' => $password->account->username, 'username' => $password->account->username,
@ -433,14 +441,14 @@ class ApiAccountTest extends TestCase
* Retrieve the authenticated account * Retrieve the authenticated account
*/ */
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->delete($this->route.'/me') ->delete($this->route . '/me')
->assertStatus(200); ->assertStatus(200);
/** /**
* Check again * Check again
*/ */
$this->get($this->route.'/'.$password->account->identifier.'/info') $this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(404); ->assertStatus(404);
} }
public function testActivateEmail() public function testActivateEmail()
@ -457,36 +465,36 @@ class ApiAccountTest extends TestCase
$expiration->expires = Carbon::now()->subYear(); $expiration->expires = Carbon::now()->subYear();
$expiration->save(); $expiration->save();
$this->get($this->route.'/'.$password->account->identifier.'/info') $this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false 'activated' => false
]); ]);
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/blabla/activate/email', [ ->json($this->method, $this->route . '/blabla/activate/email', [
'code' => $confirmationKey 'confirmation_key' => $confirmationKey
]) ])
->assertStatus(404); ->assertStatus(404);
$activateEmailRoute = $this->route.'/'.$password->account->identifier.'/activate/email'; $activateEmailRoute = $this->route . '/' . $password->account->identifier . '/activate/email';
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [ ->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey.'longer' 'confirmation_key' => $confirmationKey . 'longer'
]) ])
->assertStatus(422); ->assertStatus(422);
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [ ->json($this->method, $activateEmailRoute, [
'code' => 'X123456789abc' 'confirmation_key' => 'X123456789abc'
]) ])
->assertStatus(404); ->assertStatus(404);
// Expired // Expired
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [ ->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey 'confirmation_key' => $confirmationKey
]) ])
->assertStatus(403); ->assertStatus(403);
@ -494,11 +502,11 @@ class ApiAccountTest extends TestCase
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [ ->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey 'confirmation_key' => $confirmationKey
]) ])
->assertStatus(200); ->assertStatus(200);
$this->get($this->route.'/'.$password->account->identifier.'/info') $this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => true 'activated' => true
@ -532,6 +540,49 @@ class ApiAccountTest extends TestCase
->assertJsonValidationErrors(['email']); ->assertJsonValidationErrors(['email']);
} }
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 * /!\ Dangerous endpoints
*/ */
@ -552,21 +603,44 @@ class ApiAccountTest extends TestCase
'activated' => false 'activated' => false
]); ]);
$this->get($this->route.'/'.$password->account->identifier.'/recover/'.$confirmationKey) $this->get($this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
->assertJson(['passwords' => [[ ->assertJson(['passwords' => [[
'password' => $password->password, 'password' => $password->password,
'algorithm' => $password->algorithm 'algorithm' => $password->algorithm
]]]) ]]])
->assertStatus(200); ->assertStatus(200);
$this->json('GET', $this->route.'/'.$password->account->identifier.'/recover/'.$confirmationKey) $this->json('GET', $this->route . '/' . $password->account->identifier . '/recover/' . $confirmationKey)
->assertStatus(404); ->assertStatus(404);
$this->assertDatabaseHas('accounts', [ $this->assertDatabaseHas('accounts', [
'username' => $password->account->username, 'username' => $password->account->username,
'domain' => $password->account->domain, 'domain' => $password->account->domain,
'confirmation_key' => null,
'activated' => true '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 +663,79 @@ class ApiAccountTest extends TestCase
$alias->account_id = $password->account->id; $alias->account_id = $password->account->id;
$alias->save(); $alias->save();
$this->json($this->method, $this->route.'/recover-by-phone', [ $this->json($this->method, $this->route . '/recover-by-phone', [
'phone' => $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); ->assertStatus(200);
$password->account->refresh(); $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) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => true 'activated' => true
]); ]);
$this->get($this->route.'/'.$phone.'/info-by-phone') $this->get($this->route . '/' . $phone . '/info-by-phone')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->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); ->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) ->assertStatus(422)
->assertJsonValidationErrors(['phone']); ->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 * /!\ Dangerous endpoints
*/ */
public function testCreatePublic() public function testCreatePublic()
{ {
$username = 'publicuser'; $username = 'publicuser';
@ -626,50 +743,83 @@ class ApiAccountTest extends TestCase
config()->set('app.dangerous_endpoints', true); config()->set('app.dangerous_endpoints', true);
// Missing email // Missing email
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonValidationErrors(['email']); ->assertJsonValidationErrors(['email']);
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
]) ])
->assertStatus(200) ->assertStatus(422)
->assertJson([ ->assertJsonValidationErrors(['account_creation_token']);
'activated' => false
]); $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 // Already created
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'username' => $username, 'username' => $username,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonValidationErrors(['username']); ->assertJsonValidationErrors(['username']);
// Email is now unique // Email is now unique
config()->set('app.account_email_unique', true); config()->set('app.account_email_unique', true);
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'username' => 'johndoe', 'username' => 'johndoe',
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonValidationErrors(['email']); ->assertJsonValidationErrors(['email']);
$this->assertDatabaseHas('accounts', [ $this->assertDatabaseHas('accounts', [
'username' => $username, '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 +829,39 @@ class ApiAccountTest extends TestCase
config()->set('app.dangerous_endpoints', true); 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 // Bad phone format
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'phone' => 'username', 'phone' => 'username',
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonValidationErrors(['phone']); ->assertJsonValidationErrors(['phone']);
$this->json($this->method, $this->route.'/public', [ $token = AccountCreationToken::factory()->create();
$this->json($this->method, $this->route . '/public', [
'phone' => $phone, 'phone' => $phone,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
'account_creation_token' => $token->token
]) ])
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false 'activated' => false
]); ]);
// Already exists // Already exists
$this->json($this->method, $this->route.'/public', [ $this->json($this->method, $this->route . '/public', [
'phone' => $phone, 'phone' => $phone,
'algorithm' => 'SHA-256', 'algorithm' => 'SHA-256',
'password' => '2', 'password' => '2',
'email' => 'john@doe.tld', 'email' => 'john@doe.tld',
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonValidationErrors(['phone']); ->assertJsonValidationErrors(['phone']);
$this->assertDatabaseHas('accounts', [ $this->assertDatabaseHas('accounts', [
'username' => $phone, 'username' => $phone,
@ -746,7 +888,7 @@ class ApiAccountTest extends TestCase
$expiration->expires = Carbon::now()->subYear(); $expiration->expires = Carbon::now()->subYear();
$expiration->save(); $expiration->save();
$this->get($this->route.'/'.$password->account->identifier.'/info') $this->get($this->route . '/' . $password->account->identifier . '/info')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false 'activated' => false
@ -754,16 +896,16 @@ class ApiAccountTest extends TestCase
// Expired // Expired
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [ ->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
'code' => $confirmationKey 'confirmation_key' => $confirmationKey
]) ])
->assertStatus(403); ->assertStatus(403);
$expiration->delete(); $expiration->delete();
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [ ->json($this->method, $this->route . '/' . $password->account->identifier . '/activate/phone', [
'code' => $confirmationKey 'confirmation_key' => $confirmationKey
]) ])
->assertStatus(200); ->assertStatus(200);
@ -783,27 +925,27 @@ class ApiAccountTest extends TestCase
// Bad email // Bad email
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/me/email/request', [ ->json($this->method, $this->route . '/me/email/request', [
'email' => 'gnap' 'email' => 'gnap'
]) ])
->assertStatus(422); ->assertStatus(422);
// Same email // Same email
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/me/email/request', [ ->json($this->method, $this->route . '/me/email/request', [
'email' => $password->account->email 'email' => $password->account->email
]) ])
->assertStatus(422); ->assertStatus(422);
// Correct email // Correct email
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/me/email/request', [ ->json($this->method, $this->route . '/me/email/request', [
'email' => $newEmail 'email' => $newEmail
]) ])
->assertStatus(200); ->assertStatus(200);
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->get($this->route.'/me') ->get($this->route . '/me')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'username' => $password->account->username, 'username' => $password->account->username,
@ -816,7 +958,7 @@ class ApiAccountTest extends TestCase
config()->set('app.account_email_unique', true); config()->set('app.account_email_unique', true);
$this->keyAuthenticated($password->account) $this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/me/email/request', [ ->json($this->method, $this->route . '/me/email/request', [
'email' => $otherAccount->account->email 'email' => $otherAccount->account->email
]) ])
->assertStatus(422) ->assertStatus(422)
@ -834,7 +976,7 @@ class ApiAccountTest extends TestCase
// Wrong algorithm // Wrong algorithm
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [ ->json($this->method, $this->route . '/me/password', [
'algorithm' => '123', 'algorithm' => '123',
'password' => $password 'password' => $password
]) ])
@ -843,7 +985,7 @@ class ApiAccountTest extends TestCase
// Fresh password without an old one // Fresh password without an old one
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [ ->json($this->method, $this->route . '/me/password', [
'algorithm' => $algorithm, 'algorithm' => $algorithm,
'password' => $password 'password' => $password
]) ])
@ -851,7 +993,7 @@ class ApiAccountTest extends TestCase
// First check // First check
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->get($this->route.'/me') ->get($this->route . '/me')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'username' => $account->username, 'username' => $account->username,
@ -862,7 +1004,7 @@ class ApiAccountTest extends TestCase
// Set new password without old one // Set new password without old one
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [ ->json($this->method, $this->route . '/me/password', [
'algorithm' => $newAlgorithm, 'algorithm' => $newAlgorithm,
'password' => $newPassword 'password' => $newPassword
]) ])
@ -871,7 +1013,7 @@ class ApiAccountTest extends TestCase
// Set the new password with incorrect old password // Set the new password with incorrect old password
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [ ->json($this->method, $this->route . '/me/password', [
'algorithm' => $newAlgorithm, 'algorithm' => $newAlgorithm,
'old_password' => 'blabla', 'old_password' => 'blabla',
'password' => $newPassword 'password' => $newPassword
@ -881,7 +1023,7 @@ class ApiAccountTest extends TestCase
// Set the new password // Set the new password
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->json($this->method, $this->route.'/me/password', [ ->json($this->method, $this->route . '/me/password', [
'algorithm' => $newAlgorithm, 'algorithm' => $newAlgorithm,
'old_password' => $password, 'old_password' => $password,
'password' => $newPassword 'password' => $newPassword
@ -890,7 +1032,7 @@ class ApiAccountTest extends TestCase
// Second check // Second check
$this->keyAuthenticated($account) $this->keyAuthenticated($account)
->get($this->route.'/me') ->get($this->route . '/me')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'username' => $account->username, 'username' => $account->username,
@ -909,28 +1051,28 @@ class ApiAccountTest extends TestCase
// deactivate // deactivate
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->id.'/deactivate') ->get($this->route . '/' . $password->account->id . '/deactivate')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false 'activated' => false
]); ]);
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->id) ->get($this->route . '/' . $password->account->id)
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => false 'activated' => false
]); ]);
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->id.'/activate') ->get($this->route . '/' . $password->account->id . '/activate')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => true 'activated' => true
]); ]);
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->id) ->get($this->route . '/' . $password->account->id)
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'activated' => true 'activated' => true
@ -938,12 +1080,44 @@ class ApiAccountTest extends TestCase
// Search feature // Search feature
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->identifier.'/search') ->get($this->route . '/' . $password->account->identifier . '/search')
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'id' => $password->account->id, 'id' => $password->account->id,
'activated' => true '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() public function testGetAll()
@ -963,7 +1137,7 @@ class ApiAccountTest extends TestCase
// /accounts/id // /accounts/id
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$admin->id) ->get($this->route . '/' . $admin->id)
->assertStatus(200) ->assertStatus(200)
->assertJson([ ->assertJson([
'id' => 1, 'id' => 1,
@ -1024,13 +1198,13 @@ class ApiAccountTest extends TestCase
$admin->account->generateApiKey(); $admin->account->generateApiKey();
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->delete($this->route.'/'.$password->account->id) ->delete($this->route . '/' . $password->account->id)
->assertStatus(200); ->assertStatus(200);
$this->assertEquals(1, AccountTombstone::count()); $this->assertEquals(1, AccountTombstone::count());
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route.'/'.$password->account->id) ->get($this->route . '/' . $password->account->id)
->assertStatus(404); ->assertStatus(404);
} }
} }

View file

@ -47,17 +47,26 @@ class ApiAccountTypeTest extends TestCase
$this->assertEquals(1, AccountType::count()); $this->assertEquals(1, AccountType::count());
// Same key
$this->keyAuthenticated($admin->account)
->json($this->method, $this->route, [
'key' => 'phone',
])
->assertJsonValidationErrorFor('key')
->assertStatus(422);
// Missing key // Missing key
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->json($this->method, $this->route, []) ->json($this->method, $this->route, [])
->assertStatus(422); ->assertStatus(422);
// Invalid key // Invalid key
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->json($this->method, $this->route, [ ->json($this->method, $this->route, [
'key' => 'Abc1234', 'key' => 'Abc1234',
]) ])
->assertStatus(422); ->assertStatus(422);
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get($this->route) ->get($this->route)
@ -83,7 +92,7 @@ class ApiAccountTypeTest extends TestCase
$accountType = AccountType::first(); $accountType = AccountType::first();
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->delete($this->route.'/'.$accountType->id) ->delete($this->route . '/' . $accountType->id)
->assertStatus(200); ->assertStatus(200);
$this->assertEquals(0, AccountType::count()); $this->assertEquals(0, AccountType::count());
@ -104,7 +113,7 @@ class ApiAccountTypeTest extends TestCase
$accountType = AccountType::first(); $accountType = AccountType::first();
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->json('PUT', $this->route.'/'.$accountType->id, [ ->json('PUT', $this->route . '/' . $accountType->id, [
'key' => 'door', 'key' => 'door',
]) ])
->assertStatus(200); ->assertStatus(200);
@ -137,34 +146,34 @@ class ApiAccountTypeTest extends TestCase
$password = Password::factory()->create(); $password = Password::factory()->create();
$this->keyAuthenticated($admin->account) $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); ->assertStatus(200);
$this->keyAuthenticated($admin->account) $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); ->assertStatus(403);
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->get('/api/accounts/'.$password->account->id) ->get('/api/accounts/' . $password->account->id)
->assertJson([ ->assertJson([
'types' => [ 'types' => [
[ [
'id' => $accountType->id, 'id' => $accountType->id,
'key' => $accountType->key 'key' => $accountType->key
]
] ]
] ]);
]);
// Remove // Remove
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id) ->delete('/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
->assertStatus(200); ->assertStatus(200);
$this->assertEquals(0, DB::table('account_account_type')->count()); $this->assertEquals(0, DB::table('account_account_type')->count());
// Retry // Retry
$this->keyAuthenticated($admin->account) $this->keyAuthenticated($admin->account)
->delete('/api/accounts/'.$password->account->id.'/types/'.$accountType->id) ->delete('/api/accounts/' . $password->account->id . '/types/' . $accountType->id)
->assertStatus(403); ->assertStatus(403);
$this->assertEquals(0, DB::table('account_account_type')->count()); $this->assertEquals(0, DB::table('account_account_type')->count());
} }