diff --git a/flexiapi/app/Http/Controllers/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Admin/AccountController.php index d768cc0..b67313d 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountController.php @@ -21,7 +21,6 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use Illuminate\Support\Str; use Illuminate\Support\Facades\Log; use Carbon\Carbon; @@ -31,7 +30,6 @@ use App\Alias; use App\ExternalAccount; use App\Http\Requests\CreateAccountRequest; use App\Http\Requests\UpdateAccountRequest; -use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Rules\BlacklistedUsername; use App\Rules\IsNotPhoneNumber; use App\Rules\NoUppercase; diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php index b81ec89..e7a6df7 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php @@ -33,11 +33,13 @@ use App\ActivationExpiration; use App\Admin; use App\Alias; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; +use App\Mail\PasswordAuthentication; use App\Rules\BlacklistedUsername; use App\Rules\IsNotPhoneNumber; use App\Rules\NoUppercase; use App\Rules\SIPUsername; use App\Rules\WithoutSpaces; +use Illuminate\Support\Facades\Mail; class AccountController extends Controller { @@ -56,6 +58,11 @@ class AccountController extends Controller return Account::sip($sip)->firstOrFail(); } + public function searchByEmail(Request $request, string $email) + { + return Account::where('email', $email)->firstOrFail(); + } + public function destroy($id) { $account = Account::findOrFail($id); @@ -183,7 +190,7 @@ class AccountController extends Controller // Full reload $account = Account::withoutGlobalScopes()->find($account->id); - Log::channel('events')->info('API: Admin: Account created', ['id' => $account->identifier]); + Log::channel('events')->info('API Admin: Account created', ['id' => $account->identifier]); return response()->json($account->makeVisible(['confirmation_key', 'provisioning_token'])); } @@ -207,4 +214,18 @@ class AccountController extends Controller return Account::findOrFail($id)->types()->detach($typeId); } + + public function recoverByEmail(int $id) + { + $account = Account::findOrFail($id); + $account->provision(); + $account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize); + $account->save(); + + Log::channel('events')->info('API Admin: Sending recovery email', ['id' => $account->identifier]); + + Mail::to($account)->send(new PasswordAuthentication($account)); + + return response()->json($account->makeVisible(['confirmation_key', 'provisioning_token'])); + } } diff --git a/flexiapi/database/migrations/2023_05_09_145418_add_email_index_to_accounts_table.php b/flexiapi/database/migrations/2023_05_09_145418_add_email_index_to_accounts_table.php new file mode 100644 index 0000000..e76453c --- /dev/null +++ b/flexiapi/database/migrations/2023_05_09_145418_add_email_index_to_accounts_table.php @@ -0,0 +1,22 @@ +index('email'); + }); + } + + public function down() + { + Schema::table('accounts', function (Blueprint $table) { + $table->dropIndex('accounts_email_index'); + }); + } +} diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index b7cf3b7..dd81bad 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -279,10 +279,18 @@ Retrieve all the accounts, paginated. Admin Retrieve a specific account. +### `POST /accounts/{id}/recover-by-email` +Admin +Send the account recovery email containing a fresh `provisioning_token` and `confirmation_key` + ### `GET /accounts/{sip}/search` Admin Search for a specific account by sip address. +### `GET /accounts/{email}/search-by-email` +Admin +Search for a specific account by email. + ### `DELETE /accounts/{id}` Admin Delete a specific account and its related information. diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 03e304d..92e6190 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -84,9 +84,12 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); Route::get('accounts/{id}/provision', 'Api\Admin\AccountController@provision'); + Route::post('accounts/{id}/recover-by-email', 'Api\Admin\AccountController@recoverByEmail'); + Route::post('accounts', 'Api\Admin\AccountController@store'); Route::get('accounts', 'Api\Admin\AccountController@index'); Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search'); + Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail'); Route::get('accounts/{id}', 'Api\Admin\AccountController@show'); Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy'); diff --git a/flexiapi/tests/Feature/AccountProvisioningTest.php b/flexiapi/tests/Feature/AccountProvisioningTest.php index c35c4af..189b84f 100644 --- a/flexiapi/tests/Feature/AccountProvisioningTest.php +++ b/flexiapi/tests/Feature/AccountProvisioningTest.php @@ -188,9 +188,9 @@ class AccountProvisioningTest extends TestCase ->assertStatus(201) ->assertJson([ 'token' => true - ])->content(); + ]); - $authToken = json_decode($response)->token; + $authToken = $response->json('token'); $password = Password::factory()->create(); $password->account->generateApiKey(); diff --git a/flexiapi/tests/Feature/ApiAccountApiKeyTest.php b/flexiapi/tests/Feature/ApiAccountApiKeyTest.php index 6a87c44..026b478 100644 --- a/flexiapi/tests/Feature/ApiAccountApiKeyTest.php +++ b/flexiapi/tests/Feature/ApiAccountApiKeyTest.php @@ -64,9 +64,9 @@ class ApiAccountApiKeyTest extends TestCase ->assertStatus(201) ->assertJson([ 'token' => true - ])->content(); + ]); - $authToken = json_decode($response)->token; + $authToken = $response->json('token'); // Try to retrieve an API key from the un-attached auth_token $response = $this->json($this->method, $this->route . '/' . $authToken) @@ -95,9 +95,9 @@ class ApiAccountApiKeyTest extends TestCase ->assertStatus(200) ->assertJson([ 'api_key' => true - ])->content(); + ]); - $apiKey = json_decode($response)->api_key; + $apiKey = $response->json('api_key'); // Re-retrieve $this->json($this->method, $this->route . '/' . $authToken) @@ -106,8 +106,7 @@ class ApiAccountApiKeyTest extends TestCase // Check the if the API key can be used for the account $response = $this->withHeaders(['x-api-key' => $apiKey]) ->json($this->method, '/api/accounts/me') - ->assertStatus(200) - ->content(); + ->assertStatus(200); // Try with a wrong From $response = $this->withHeaders([ @@ -115,10 +114,9 @@ class ApiAccountApiKeyTest extends TestCase 'From' => 'sip:baduser@server.tld' ]) ->json($this->method, '/api/accounts/me') - ->assertStatus(200) - ->content(); + ->assertStatus(200); // Check if the account was correctly attached - $this->assertEquals(json_decode($response)->email, $password->account->email); + $this->assertEquals($response->json('email'), $password->account->email); } } diff --git a/flexiapi/tests/Feature/ApiAccountTest.php b/flexiapi/tests/Feature/ApiAccountTest.php index 107ec88..0d9fd44 100644 --- a/flexiapi/tests/Feature/ApiAccountTest.php +++ b/flexiapi/tests/Feature/ApiAccountTest.php @@ -1007,6 +1007,38 @@ class ApiAccountTest extends TestCase 'id' => $password->account->id, 'activated' => true ]); + + $this->keyAuthenticated($admin->account) + ->get($this->route . '/' . $password->account->email . '/search-by-email') + ->assertStatus(200) + ->assertJson([ + 'id' => $password->account->id, + 'activated' => true + ]); + + $this->keyAuthenticated($admin->account) + ->get($this->route . '/wrong@email.com/search-by-email') + ->assertStatus(404); + } + + public function testRecoverByEmail() + { + $email = 'collision@email.com'; + + $account = Password::factory()->create(); + $account->account->email = $email; + $account->account->save(); + + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + $admin->account->save(); + + $response = $this->keyAuthenticated($admin->account) + ->post($this->route . '/' . $account->id . '/recover-by-email') + ->assertStatus(200); + + $this->assertNotEquals($response->json('confirmation_key'), $account->confirmation_key); + $this->assertNotEquals($response->json('provisioning_token'), $account->provisioning_token); } public function testGetAll()