Fix/325 328

This commit is contained in:
Timothée Jaussoin 2025-06-11 15:36:51 +00:00
parent b590995b4e
commit 724e4c4e5b
12 changed files with 124 additions and 15 deletions

View file

@ -38,6 +38,14 @@ v2.0
- Fix FLEXIAPI-224 Add a console script to send Space Expiration emails
- Fix FLEXIAPI-297 Fix PrId and CallId validations
- Fix FLEXIAPI-305 Add specific error page for Space Expiration
- Fix FLEXIAPI-169 Added missing selinux label to log files and storage directory
- Fix FLEXIAPI-313 Fix the admin device deletion link, recover the missing...
- Fix FLEXIAPI-318 Fix email recovery validation
- Fix FLEXIAPI-319 Fix the admin device deletion link, recover the missing method
- Fix FLEXIAPI-321 Disable the account creation button when the Space is full for admins
- Fix FLEXIAPI-322 Api Keys documentation
- Fix FLEXIAPI-328 Set realm on Space creation, limit the update if some accounts are present
- Fix FLEXIAPI-325 Add endpoints to send the password reset and provisioning emails
v1.6
----

View file

@ -67,12 +67,14 @@ class SpaceController extends Controller
'domain' => ['required', 'unique:spaces', new Domain()],
'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/',
'full_host' => ['required', 'unique:spaces,host', new Domain()],
'account_realm' => [new Domain()],
]);
$space = new Space();
$space->name = $request->get('name');
$space->domain = $request->get('domain');
$space->host = $request->get('full_host');
$space->account_realm = $request->get('account_realm');
$space->save();
return redirect()->route('admin.spaces.index');
@ -163,7 +165,11 @@ class SpaceController extends Controller
$space->confirmed_registration_text = $request->get('confirmed_registration_text');
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
$space->account_realm = $request->get('account_realm');
if ($space->accounts()->count() == 0) {
$space->account_realm = $request->get('account_realm');
}
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
$space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all');
$space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header');

View file

@ -21,14 +21,19 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Account;
use App\AccountTombstone;
use App\AccountType;
use App\ContactsList;
use App\ResetPasswordEmailToken;
use App\Http\Requests\Account\Create\Api\AsAdminRequest;
use App\Http\Requests\Account\Update\Api\AsAdminRequest as ApiAsAdminRequest;
use App\Mail\Provisioning;
use App\Mail\ResetPassword;
use App\Services\AccountService;
use App\Space;
@ -198,4 +203,36 @@ class AccountController extends Controller
return Account::findOrFail($accountId)->contactsLists()->detach($contactsListId);
}
/**
* Emails
*/
public function sendProvisioningEmail(int $accountId)
{
$account = Account::findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
$account->provision();
Mail::to($account)->send(new Provisioning($account));
Log::channel('events')->info('API: Sending provisioning email', ['id' => $account->identifier]);
}
public function sendResetPasswordEmail(int $accountId)
{
$account = Account::findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;
$resetPasswordEmail->token = Str::random(16);
$resetPasswordEmail->email = $account->email;
$resetPasswordEmail->save();
Mail::to($account)->send(new ResetPassword($account));
}
}

View file

@ -70,8 +70,6 @@ class AuthenticateDigestOrKey
->where('domain', $domain)
->firstOrFail();
$resolvedRealm = space()?->account_realm ?? $domain;
// DIGEST authentication
if ($request->header('Authorization')) {
@ -95,7 +93,7 @@ class AuthenticateDigestOrKey
'opaque' => 'required|in:'.$this->getOpaque(),
//'uri' => 'in:/'.$request->path(),
'qop' => 'required|in:auth',
'realm' => 'required|in:'.$resolvedRealm,
'realm' => 'required|in:'.$account->resolvedRealm,
'nc' => 'required',
'cnonce' => 'required',
'algorithm' => [
@ -128,7 +126,7 @@ class AuthenticateDigestOrKey
// Hashing and checking
$a1 = $password->algorithm == 'CLRTXT'
? hash($hash, $account->username.':'.$resolvedRealm.':'.$password->password)
? hash($hash, $account->username.':'.$account->resolvedRealm.':'.$password->password)
: $password->password; // username:realm/domain:password
$a2 = hash($hash, $request->method().':'.$auth['uri']);
@ -199,21 +197,20 @@ class AuthenticateDigestOrKey
private function generateAuthHeaders(Account $account, string $nonce): array
{
$headers = [];
$resolvedRealm = space()?->account_realm ?? $account->domain;
foreach ($account->passwords as $password) {
if ($password->algorithm == 'CLRTXT') {
foreach (array_keys(passwordAlgorithms()) as $algorithm) {
array_push(
$headers,
$this->generateAuthHeader($resolvedRealm, $algorithm, $nonce)
$this->generateAuthHeader($account->resolvedRealm, $algorithm, $nonce)
);
}
break;
} elseif (\in_array($password->algorithm, array_keys(passwordAlgorithms()))) {
array_push(
$headers,
$this->generateAuthHeader($resolvedRealm, $password->algorithm, $nonce)
$this->generateAuthHeader($account->resolvedRealm, $password->algorithm, $nonce)
);
}
}

View file

@ -30,11 +30,10 @@ class PasswordFactory extends Factory
public function definition()
{
$account = Account::factory()->create();
$realm = space()?->account_realm ?? $account->domain;
return [
'account_id' => $account->id,
'password' => hash('md5', $account->username.':'.$realm.':testtest'),
'password' => hash('md5', $account->username.':'.$account->resolvedRealm.':testtest'),
'algorithm' => 'MD5',
];
}
@ -54,10 +53,9 @@ class PasswordFactory extends Factory
{
return $this->state(function (array $attributes) {
$account = Account::find($attributes['account_id']);
$realm = space()?->account_realm ?? $account->domain;
return [
'password' => hash('sha256', $account->username.':'.$realm.':testtest'),
'password' => hash('sha256', $account->username.':'.$account->resolvedRealm.':testtest'),
'account_id' => $account->id,
'algorithm' => 'SHA-256',
];

View file

@ -40,7 +40,7 @@
"Change your email": "Changer votre email",
"Change your phone number": "Changer votre numéro de téléphone",
"Code Verification" : "Vérification du code",
"instant messaging": "messagerie instantanée",
"Instant Messaging": "Messagerie Instantanée",
"Check the README.md documentation": "Voir la documentation dans README.md",
"Clear to never expire": "Laisser vide pour ne jamais expirer",
"Code": "Code",

View file

@ -68,7 +68,7 @@
</div>
<div>
<input name="account_realm" id="account_realm" placeholder="server.tld" value="{{ $space->account_realm }}">
<input name="account_realm" @if ($space->accounts()->count() > 0)disabled @endif id="account_realm" placeholder="server.tld" value="{{ $space->account_realm }}">
<label for="account_realm">Account realm</label>
<span class="supporting">A custom realm for the Space accounts</span>
@include('parts.errors', ['name' => 'account_realm'])

View file

@ -44,6 +44,14 @@
@include('parts.form.toggle', ['object' => $space, 'key' => 'super', 'label' => __('Super space'), 'supporting' => __('All the admins will be super admins')])
<div>
<input placeholder="realp.sip" name="account_realm" type="text" pattern="{{ $space::DOMAIN_REGEX}}" value="{{ $space->account_realm ?? old('account_realm') }}">
<label for="username">{{ __('Account realm') }}</label>
@include('parts.errors', ['name' => 'account_realm'])
<span class="supporting">{{ __('Leave empty if similar to the domain') }}</span>
</div>
<div class="large">
<input class="btn" type="submit" value="{{ __('Create') }}">
</div>

View file

@ -27,7 +27,7 @@
<h3 class="large">{{ __('Features') }}</h3>
@include('parts.form.toggle', ['object' => $space, 'key' => 'disable_chat_feature', 'label' => __('instant messaging'), 'reversed' => true])
@include('parts.form.toggle', ['object' => $space, 'key' => 'disable_chat_feature', 'label' => __('Instant Messaging'), 'reversed' => true])
@include('parts.form.toggle', ['object' => $space, 'key' => 'disable_meetings_feature', 'label' => __('Meeting'), 'reversed' => true])
@include('parts.form.toggle', ['object' => $space, 'key' => 'disable_broadcast_feature', 'label' => __('Broadcast'), 'reversed' => true])
@include('parts.form.toggle', ['object' => $space, 'key' => 'hide_settings', 'label' => __('App settings'), 'reversed' => true])

View file

@ -501,6 +501,16 @@ Unblock an account.
Provision an account by generating a fresh `provisioning_token`.
### `POST /accounts/{id}/send_provisioning_email`
<span class="badge badge-warning">Admin</span>
Send a provisioning email to the account.
### `POST /accounts/{id}/send_reset_password_email`
<span class="badge badge-warning">Admin</span>
Send a password reset email to the account.
## Accounts email
### `POST /accounts/me/email/request`

View file

@ -118,6 +118,8 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
Route::post('{account_id}/block', 'block');
Route::post('{account_id}/unblock', 'unblock');
Route::get('{account_id}/provision', 'provision');
Route::post('{account_id}/send_provisioning_email', 'sendProvisioningEmail');
Route::post('{account_id}/send_reset_password_email', 'sendResetPasswordEmail');
Route::post('/', 'store');
Route::put('{account_id}', 'update');

View file

@ -669,6 +669,49 @@ class ApiAccountTest extends TestCase
->json('GET', '/api/accounts/me');
}
public function testSendProvisioningEmail()
{
$password = Password::factory()->create();
$account = $password->account;
$admin = Account::factory()->admin()->create();
$admin->generateUserApiKey();
$admin->save();
$this->keyAuthenticated($admin)
->json('POST', $this->route . '/' . $account->id . '/send_provisioning_email')
->assertStatus(403);
$account->email = 'test@email.com';
$account->save();
$this->keyAuthenticated($admin)
->json('POST', $this->route . '/' . $account->id . '/send_provisioning_email')
->assertStatus(200);
}
public function testSendResetPasswordEmail()
{
$password = Password::factory()->create();
$account = $password->account;
$admin = Account::factory()->admin()->create();
$admin->generateUserApiKey();
$admin->save();
$this->keyAuthenticated($admin)
->json('POST', $this->route . '/' . $account->id . '/send_reset_password_email')
->assertStatus(403);
$account->email = 'test@email.com';
$account->save();
$this->keyAuthenticated($admin)
->json('POST', $this->route . '/' . $account->id . '/send_reset_password_email')
->assertStatus(200);
}
public function testEditAdmin()
{
$password = Password::factory()->create();