Fix #139 Allow the dictionary to be set when creating an account on the API as an admin

This commit is contained in:
Timothée Jaussoin 2023-12-28 15:17:01 +00:00
parent 0597db0f8e
commit 6226e867ad
12 changed files with 138 additions and 87 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,24 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class Dictionary implements Rule
{
public function passes($attribute, $array): bool
{
if (!is_array($array)) return false;
foreach ($array as $key => $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';
}
}

42
flexiapi/composer.lock generated
View file

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

View file

@ -174,6 +174,7 @@ code {
}
p>a:not(.btn),
li>a,
table tr td a:not(.btn):hover,
label>a {
text-decoration: underline;

View file

@ -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, <a href="#dictionary">see also the related endpoints</a>.
* <span class="badge badge-message">Deprecated</span> `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}`

View file

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

View file

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