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)