diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e90a3..6f11330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v1.5 ---- +- Fix #138 Add a dictionary attached to the accounts - Fix #137 Migrate the icons from Material Icons to Material Symbols - Fix #135 Refactor the password algorithms code - Fix #134 Create an Activity view in the Admin > Accounts panel diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 5fed7e4..82514f4 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -157,6 +157,22 @@ class Account extends Authenticatable }); } + public function setDictionaryEntry(string $key, string $value): AccountDictionaryEntry + { + $entry = $this->dictionaryEntries->where('key', $key)->first(); + + if (!$entry) { + $entry = new AccountDictionaryEntry; + } + + $entry->account_id = $this->id; + $entry->key = $key; + $entry->value = $value; + $entry->save(); + + return $entry; + } + public function nonces() { return $this->hasMany(DigestNonce::class); diff --git a/flexiapi/app/Http/Controllers/Admin/AccountDictionaryController.php b/flexiapi/app/Http/Controllers/Admin/AccountDictionaryController.php index a0b1e17..026e43d 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountDictionaryController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountDictionaryController.php @@ -52,11 +52,7 @@ class AccountDictionaryController extends Controller 'value' => 'required' ]); - $entry = new AccountDictionaryEntry; - $entry->account_id = $account->id; - $entry->key = $request->get('key'); - $entry->value = $request->get('value'); - $entry->save(); + $account->setDictionaryEntry($request->get('key'), $request->get('value')); return redirect()->route('admin.account.dictionary.index', $account->id); } diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php index 0a612f8..e3635a6 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php @@ -144,6 +144,12 @@ class AccountController extends Controller $actionvationExpiration->save(); } + if ($request->has('dictionary')) { + foreach ($request->get('dictionary') as $key => $value) { + $account->setDictionaryEntry($key, $value); + } + } + $account->updatePassword($request->get('password'), $request->get('algorithm')); $account->admin = $request->has('admin') && (bool)$request->get('admin'); $account->phone = $request->get('phone'); diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountDictionaryController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountDictionaryController.php index d1808f0..664f8f5 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountDictionaryController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountDictionaryController.php @@ -43,18 +43,7 @@ class AccountDictionaryController extends Controller 'value' => 'required' ]); - $entry = Account::findOrFail($accountId)->dictionaryEntries()->where('key', $key)->first(); - - if (!$entry) { - $entry = new AccountDictionaryEntry; - } - - $entry->account_id = $accountId; - $entry->key = $key; - $entry->value = $request->get('value'); - $entry->save(); - - return $entry; + return Account::findOrFail($accountId)->setDictionaryEntry($key, $request->get('value')); } public function destroy(Request $request, int $accountId, string $key) diff --git a/flexiapi/app/Http/Requests/CreateAccountRequest.php b/flexiapi/app/Http/Requests/CreateAccountRequest.php index dc01f0f..c85de43 100644 --- a/flexiapi/app/Http/Requests/CreateAccountRequest.php +++ b/flexiapi/app/Http/Requests/CreateAccountRequest.php @@ -7,6 +7,7 @@ use Illuminate\Validation\Rule; use App\Account; use App\Rules\BlacklistedUsername; +use App\Rules\Dictionary; use App\Rules\IsNotPhoneNumber; use App\Rules\NoUppercase; use App\Rules\SIPUsername; @@ -36,6 +37,7 @@ class CreateAccountRequest extends FormRequest }),*/ 'filled', ], + 'dictionary' => [new Dictionary], 'password' => 'required|min:3', 'email' => config('app.account_email_unique') ? 'nullable|email|unique:accounts,email' diff --git a/flexiapi/app/Rules/Dictionary.php b/flexiapi/app/Rules/Dictionary.php new file mode 100644 index 0000000..2d8a6cd --- /dev/null +++ b/flexiapi/app/Rules/Dictionary.php @@ -0,0 +1,24 @@ + $value) { + if (!is_string($key) || !is_string($value)) return false; + } + + return true; + } + + public function message() + { + return 'The dictionary must be an assiocative dictionary of strings'; + } +} diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index 26bf0f5..0961f17 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -3570,23 +3570,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -3636,7 +3636,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -3644,7 +3644,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5572,20 +5572,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5617,7 +5617,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -5625,7 +5625,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -5899,20 +5899,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -5944,7 +5944,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -5952,7 +5952,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", diff --git a/flexiapi/public/css/style.css b/flexiapi/public/css/style.css index 97d2dec..83f9569 100644 --- a/flexiapi/public/css/style.css +++ b/flexiapi/public/css/style.css @@ -174,6 +174,7 @@ code { } p>a:not(.btn), +li>a, table tr td a:not(.btn):hover, label>a { text-decoration: underline; diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index 02143df..241692b 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -336,6 +336,7 @@ JSON parameters: * `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` +* `dictionary` optional, an associative array attached to the account, see also the related endpoints. * Deprecated `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}` diff --git a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php index 28fd8dd..470e497 100644 --- a/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php +++ b/flexiapi/tests/Feature/ApiAccountCreationTokenTest.php @@ -152,7 +152,6 @@ class ApiAccountCreationTokenTest extends TestCase 'password' => '123', 'account_creation_token' => $token->token ]); - $response->assertStatus(422); $response->assertJsonValidationErrors(['username']); // Blacklisted regex username @@ -163,7 +162,6 @@ class ApiAccountCreationTokenTest extends TestCase 'account_creation_token' => $token->token ]); - $response->assertStatus(422); $response->assertJsonValidationErrors(['username']); // Valid username diff --git a/flexiapi/tests/Feature/ApiAccountTest.php b/flexiapi/tests/Feature/ApiAccountTest.php index 6582fe9..7a7b109 100644 --- a/flexiapi/tests/Feature/ApiAccountTest.php +++ b/flexiapi/tests/Feature/ApiAccountTest.php @@ -343,6 +343,53 @@ class ApiAccountTest extends TestCase $this->assertFalse(empty($response1['provisioning_token'])); } + public function testAdminWithDictionary() + { + $admin = Admin::factory()->create(); + $password = $admin->account->passwords()->first(); + $password->account->generateApiKey(); + + $entryKey = 'foo'; + $entryValue = 'bar'; + + $response = $this->keyAuthenticated($password->account) + ->json($this->method, $this->route, [ + 'username' => 'john', + 'domain' => 'lennon.com', + 'password' => 'password123', + 'algorithm' => 'SHA-256', + 'dictionary' => [ + $entryKey => $entryValue + ] + ]) + ->assertStatus(200) + ->assertJson([ + 'dictionary' => [ + $entryKey => $entryValue + ] + ]); + + $response = $this->keyAuthenticated($password->account) + ->json($this->method, $this->route, [ + 'username' => 'john2', + 'domain' => 'lennon.com', + 'password' => 'password123', + 'algorithm' => 'SHA-256', + 'dictionary' => [ + $entryKey => ['hey' => 'hop'] + ] + ])->assertJsonValidationErrors(['dictionary']); + + $response = $this->keyAuthenticated($password->account) + ->json($this->method, $this->route, [ + 'username' => 'john2', + 'domain' => 'lennon.com', + 'password' => 'password123', + 'algorithm' => 'SHA-256', + 'dictionary' => 'hop' + ])->assertJsonValidationErrors(['dictionary']); + } + public function testActivated() { $admin = Admin::factory()->create(); @@ -534,9 +581,7 @@ class ApiAccountTest extends TestCase 'domain' => 'server.com', 'algorithm' => 'SHA-256', 'password' => '123456', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ])->assertJsonValidationErrors(['email']); } public function testNonAsciiPasswordAdmin() @@ -580,7 +625,6 @@ class ApiAccountTest extends TestCase $this->keyAuthenticated($admin->account) ->json('PUT', $this->route . '/1234') - ->assertStatus(422) ->assertJsonValidationErrors(['username']); $this->keyAuthenticated($admin->account) @@ -707,23 +751,19 @@ class ApiAccountTest extends TestCase $this->json($this->method, $this->route . '/recover-by-phone', [ 'phone' => $phone - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['account_creation_token']); + ])->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']); + ])->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(); @@ -731,8 +771,7 @@ class ApiAccountTest extends TestCase $this->json($this->method, $this->route . '/recover-by-phone', [ 'phone' => $phone, 'account_creation_token' => $token->token - ]) - ->assertStatus(422); + ])->assertStatus(422); $this->get($this->route . '/' . $password->account->identifier . '/recover/' . $password->account->confirmation_key) ->assertStatus(200) @@ -751,7 +790,6 @@ class ApiAccountTest extends TestCase ->assertStatus(404); $this->json('GET', $this->route . '/' . $password->account->identifier . '/info-by-phone') - ->assertStatus(422) ->assertJsonValidationErrors(['phone']); // Check the mixed username/phone resolution... @@ -788,18 +826,14 @@ class ApiAccountTest extends TestCase 'username' => $username, 'algorithm' => 'SHA-256', 'password' => '2', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ])->assertJsonValidationErrors(['email']); $this->json($this->method, $this->route . '/public', [ 'username' => $username, 'algorithm' => 'SHA-256', 'password' => '2', 'email' => 'john@doe.tld', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['account_creation_token']); + ])->assertJsonValidationErrors(['account_creation_token']); $token = AccountCreationToken::factory()->create(); $userAgent = 'User Agent Test'; @@ -827,8 +861,7 @@ class ApiAccountTest extends TestCase 'password' => '2', 'email' => 'john@doe.tld', 'account_creation_token' => $token->token - ]) - ->assertStatus(422); + ])->assertStatus(422); // Already created $this->json($this->method, $this->route . '/public', [ @@ -836,9 +869,7 @@ class ApiAccountTest extends TestCase 'algorithm' => 'SHA-256', 'password' => '2', 'email' => 'john@doe.tld', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['username']); + ])->assertJsonValidationErrors(['username']); // Email is now unique config()->set('app.account_email_unique', true); @@ -848,9 +879,7 @@ class ApiAccountTest extends TestCase 'algorithm' => 'SHA-256', 'password' => '2', 'email' => 'john@doe.tld', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ])->assertJsonValidationErrors(['email']); $this->assertDatabaseHas('accounts', [ 'username' => $username, @@ -876,9 +905,7 @@ class ApiAccountTest extends TestCase 'algorithm' => 'SHA-256', 'password' => '2', 'email' => 'john@doe.tld', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['phone']); + ])->assertJsonValidationErrors(['phone']); $token = AccountCreationToken::factory()->create(); @@ -900,9 +927,7 @@ class ApiAccountTest extends TestCase 'algorithm' => 'SHA-256', 'password' => '2', 'email' => 'john@doe.tld', - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['phone']); + ])->assertJsonValidationErrors(['phone']); $this->assertDatabaseHas('accounts', [ 'username' => $phone, @@ -1001,9 +1026,7 @@ class ApiAccountTest extends TestCase $this->keyAuthenticated($password->account) ->json($this->method, $this->route . '/me/email/request', [ 'email' => $otherAccount->account->email - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['email']); + ])->assertJsonValidationErrors(['email']); } public function testChangePassword() @@ -1020,9 +1043,7 @@ class ApiAccountTest extends TestCase ->json($this->method, $this->route . '/me/password', [ 'algorithm' => '123', 'password' => $password - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['algorithm']); + ])->assertJsonValidationErrors(['algorithm']); // Fresh password without an old one $this->keyAuthenticated($account) @@ -1048,9 +1069,7 @@ class ApiAccountTest extends TestCase ->json($this->method, $this->route . '/me/password', [ 'algorithm' => $newAlgorithm, 'password' => $newPassword - ]) - ->assertStatus(422) - ->assertJsonValidationErrors(['old_password']); + ])->assertJsonValidationErrors(['old_password']); // Set the new password with incorrect old password $this->keyAuthenticated($account) @@ -1058,9 +1077,7 @@ class ApiAccountTest extends TestCase 'algorithm' => $newAlgorithm, 'old_password' => 'blabla', 'password' => $newPassword - ]) - ->assertJsonValidationErrors(['old_password']) - ->assertStatus(422); + ])->assertJsonValidationErrors(['old_password']); // Set the new password $this->keyAuthenticated($account)