diff --git a/flexiapi/app/Http/Controllers/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Admin/AccountController.php index e2cd144..9c7571e 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountController.php @@ -168,7 +168,7 @@ class AccountController extends Controller return redirect()->route('admin.account.index'); } - public function attachContactsList(Request $request, int $id) + public function contactsListAdd(Request $request, int $id) { $request->validate([ 'contacts_list_id' => 'required|exists:contacts_lists,id' @@ -181,7 +181,7 @@ class AccountController extends Controller return redirect()->route('admin.account.edit', $id); } - public function detachContactsList(Request $request, int $id) + public function contactsListRemove(Request $request, int $id) { $account = Account::findOrFail($id); $account->contactsLists()->detach([$request->get('contacts_list_id')]); diff --git a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php index 0ee61ee..2f86d2e 100644 --- a/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/Admin/AccountController.php @@ -29,6 +29,7 @@ use App\Account; use App\AccountTombstone; use App\AccountType; use App\ActivationExpiration; +use App\ContactsList; use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController; use App\Http\Requests\CreateAccountRequest; use App\Http\Requests\UpdateAccountRequest; @@ -205,6 +206,26 @@ class AccountController extends Controller return Account::findOrFail($id)->types()->detach($typeId); } + public function contactsListAdd(int $id, int $contactsListId) + { + if (Account::findOrFail($id)->contactsLists()->pluck('id')->contains($contactsListId)) { + abort(403); + } + + if (ContactsList::findOrFail($contactsListId)) { + return Account::findOrFail($id)->contactsLists()->attach($contactsListId); + } + } + + public function contactsListRemove(int $id, int $contactsListId) + { + if (!Account::findOrFail($id)->contactsLists()->pluck('id')->contains($contactsListId)) { + abort(403); + } + + return Account::findOrFail($id)->contactsLists()->detach($contactsListId); + } + public function recoverByEmail(int $id) { $account = Account::findOrFail($id); diff --git a/flexiapi/app/Http/Controllers/Api/Admin/ContactsListController.php b/flexiapi/app/Http/Controllers/Api/Admin/ContactsListController.php new file mode 100644 index 0000000..9f98a47 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/Admin/ContactsListController.php @@ -0,0 +1,79 @@ +firstOrFail(); + } + + public function store(Request $request) + { + $request->validate([ + 'title' => ['required'], + 'description' => ['required'] + ]); + + $contactsList = new ContactsList; + $contactsList->title = $request->get('title'); + $contactsList->description = $request->get('description'); + $contactsList->save(); + + return $contactsList; + } + + public function update(Request $request, int $contactsListId) + { + $request->validate([ + 'title' => ['required'], + 'description' => ['required'] + ]); + + $contactsList = ContactsList::where('id', $contactsListId) + ->firstOrFail(); + $contactsList->title = $request->get('title'); + $contactsList->description = $request->get('description'); + $contactsList->save(); + + return $contactsList; + } + + public function destroy(int $contactsListId) + { + return ContactsList::where('id', $contactsListId) + ->delete(); + } + + public function contactAdd(int $id, int $contactId) + { + if (ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) { + abort(403); + } + + if (Account::findOrFail($contactId)) { + return ContactsList::findOrFail($id)->contacts()->attach($contactId); + } + } + + public function contactRemove(int $id, int $contactId) + { + if (!ContactsList::findOrFail($id)->contacts()->pluck('id')->contains($contactId)) { + abort(403); + } + + return ContactsList::findOrFail($id)->contacts()->detach($contactId); + } +} diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index ff0f160..01f8118 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -399,16 +399,16 @@ }, { "name": "doctrine/dbal", - "version": "3.6.3", + "version": "3.6.4", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a" + "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9a747d29e7e6b39509b8f1847e37a23a0163ea6a", - "reference": "9a747d29e7e6b39509b8f1847e37a23a0163ea6a", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", + "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f", "shasum": "" }, "require": { @@ -491,7 +491,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.6.3" + "source": "https://github.com/doctrine/dbal/tree/3.6.4" }, "funding": [ { @@ -507,7 +507,7 @@ "type": "tidelift" } ], - "time": "2023-06-01T05:46:46+00:00" + "time": "2023-06-15T07:40:12+00:00" }, { "name": "doctrine/deprecations", @@ -1729,16 +1729,16 @@ }, { "name": "laravel/framework", - "version": "v9.52.9", + "version": "v9.52.10", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97" + "reference": "858add225ce88a76c43aec0e7866288321ee0ee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c512ece7b1ee393eac5893f37cb2b029a5413b97", - "reference": "c512ece7b1ee393eac5893f37cb2b029a5413b97", + "url": "https://api.github.com/repos/laravel/framework/zipball/858add225ce88a76c43aec0e7866288321ee0ee9", + "reference": "858add225ce88a76c43aec0e7866288321ee0ee9", "shasum": "" }, "require": { @@ -1923,7 +1923,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-06-08T20:06:23+00:00" + "time": "2023-06-27T13:25:54+00:00" }, { "name": "laravel/serializable-closure", @@ -2615,16 +2615,16 @@ }, { "name": "nesbot/carbon", - "version": "2.67.0", + "version": "2.68.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8" + "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/c1001b3bc75039b07f38a79db5237c4c529e04c8", - "reference": "c1001b3bc75039b07f38a79db5237c4c529e04c8", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da", + "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da", "shasum": "" }, "require": { @@ -2713,7 +2713,7 @@ "type": "tidelift" } ], - "time": "2023-05-25T22:09:47+00:00" + "time": "2023-06-20T18:29:04+00:00" }, { "name": "nette/schema", @@ -7484,16 +7484,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.5", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e" + "reference": "19526a33fb561ef417e822e85f08a00db4059c17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/11e2663a5bc9db5d714eedb4277ee300403b4a9e", - "reference": "11e2663a5bc9db5d714eedb4277ee300403b4a9e", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", + "reference": "19526a33fb561ef417e822e85f08a00db4059c17", "shasum": "" }, "require": { @@ -7534,9 +7534,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" }, - "time": "2023-05-19T20:20:00+00:00" + "time": "2023-06-25T14:52:30+00:00" }, { "name": "nunomaduro/collision", diff --git a/flexiapi/public/css/far.css b/flexiapi/public/css/far.css index 99eaa14..2e21569 100644 --- a/flexiapi/public/css/far.css +++ b/flexiapi/public/css/far.css @@ -88,6 +88,14 @@ body { --danger-7: rgba(158, 53, 72, 1); --danger-8: rgba(127, 37, 61, 1); --danger-9: rgba(104, 26, 54, 1); + + --color-orange: rgba(254, 94, 0, 1); + --color-grey: rgba(108, 122, 135, 1); + --color-green: rgba(79, 174, 128, 1); + --color-blue: rgba(9, 197, 244, 1); + --color-yellow: rgba(255, 220, 46, 1); + --color-pink: rgba(255, 94, 102, 1); + --color-purple: rgba(151, 71, 255, 1); } body.show_menu { @@ -468,6 +476,7 @@ h3 { font-size: 1.75rem; color: var(--second-6); padding: 0.5rem 0; + margin-top: 1.25rem; } h4 { @@ -488,6 +497,18 @@ h4 { padding: 0.25rem 1.25rem; } +.badge.badge-info { + background-color: var(--color-blue); +} + +.badge.badge-success { + background-color: var(--color-green); +} + +.badge.badge-warning { + background-color: var(--color-yellow); +} + /** Table **/ table { diff --git a/flexiapi/public/scripts/utils.js b/flexiapi/public/scripts/utils.js new file mode 100644 index 0000000..8210295 --- /dev/null +++ b/flexiapi/public/scripts/utils.js @@ -0,0 +1,111 @@ +/** + * @brief Set object in localStorage + * @param key string + * @param value the object + */ +Storage.prototype.setObject = function (key, value) { + this.setItem(key, JSON.stringify(value)); +}; + +/** + * @brief Get object in localStorage + * @param key + */ +Storage.prototype.getObject = function (key) { + return JSON.parse(this.getItem(key)); +}; + +var Utils = { + toggleAll: function (checkbox) { + checkbox.closest('table').querySelectorAll('tbody input[type=checkbox]').forEach(element => { + element.checked = checkbox.checked; + element.dispatchEvent(new Event('change')); + }); + }, + + getStorageList: function (key) { + var list = sessionStorage.getObject('list.' + key); + + if (list == null) { + list = []; + } + + return list; + }, + + addToStorageList: function (key, id) { + var list = Utils.getStorageList(key); + + if (!list.includes(id)) { + list.push(id); + } + + sessionStorage.setObject('list.' + key, list); + }, + + removeFromStorageList: function(key, id) { + var list = Utils.getStorageList(key); + + list.splice(list.indexOf(id), 1); + + sessionStorage.setObject('list.' + key, list); + }, + + existsInStorageList: function(key, id) { + var list = Utils.getStorageList(key); + return (list && list.includes(id)); + }, + + clearStorageList: function (key) { + sessionStorage.setObject('list.' + key, []); + }, + + /** List toggle */ +} + +var ListToggle = { + init: function() { + document.querySelectorAll('input[type=checkbox].list_toggle').forEach(checkbox => { + checkbox.checked = Utils.existsInStorageList(checkbox.dataset.listId, checkbox.dataset.id); + + checkbox.addEventListener('change', e => { + if (checkbox.checked) { + Utils.addToStorageList(checkbox.dataset.listId, checkbox.dataset.id); + } else { + Utils.removeFromStorageList(checkbox.dataset.listId, checkbox.dataset.id); + } + + ListToggle.refreshFormList(); + ListToggle.refreshCounters(); + }) + }); + + ListToggle.refreshFormList(); + ListToggle.refreshCounters(); + }, + + refreshFormList: function() { + document.querySelectorAll('select.list_toggle').forEach(select => { + select.innerHTML = ''; + select.multiple = true; + + Utils.getStorageList(select.dataset.listId).forEach(id => { + const option = document.createElement("option"); + option.value = id; + option.text = id; + option.selected = true; + select.add(option, null); + }); + }); + }, + + refreshCounters: function() { + document.querySelectorAll('span.list_toggle').forEach(counter => { + counter.innerHTML = Utils.getStorageList(counter.dataset.listId).length; + }); + } +} + +document.addEventListener("DOMContentLoaded", function(event) { + ListToggle.init(); +}); \ No newline at end of file diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index 2bb5963..fe948e9 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -428,6 +428,46 @@ JSON parameters: Admin Delete an account related action. +## Contacts Lists + +### `GET /contacts_lists` +Admin +Show all the contacts lists. + +### `GET /contacts_lists/{id}` +Admin +Show a contacts list. + +### `POST /contacts_lists` +Admin +Create a contacts list. + +JSON parameters: + +* `title` required +* `description` required + +### `PUT /contacts_lists/{id}` +Admin +Update a contacts list. + +JSON parameters: + +* `title` required +* `description` required + +### `DELETE /contacts_lists/{id}` +Admin +Delete a contacts list. + +### `POST /accounts/{id}/contacts_lists/{contacts_list_id}` +Admin +Add a contacts list to the account. + +### `DELETE /accounts/{id}/contacts_lists/{contacts_list_id}` +Admin +Remove a contacts list from the account. + ## Account Types ### `GET /account_types` @@ -464,7 +504,7 @@ Add a type to the account. ### `DELETE /accounts/{id}/contacts/{type_id}` Admin -Remove a a type from the account. +Remove a type from the account. ## Messages diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 0413d29..25c51bb 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -17,6 +17,11 @@ along with this program. If not, see . */ +use App\Http\Controllers\Api\Admin\AccountActionController; +use App\Http\Controllers\Api\Admin\AccountContactController; +use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController; +use App\Http\Controllers\Api\Admin\AccountTypeController; +use App\Http\Controllers\Api\Admin\ContactsListController; use Illuminate\Http\Request; Route::get('/', 'Api\ApiController@documentation')->name('api'); @@ -81,41 +86,43 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::post('account_creation_tokens', 'Api\Admin\AccountCreationTokenController@create'); // Accounts - Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate'); - Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); - Route::get('accounts/{id}/provision', 'Api\Admin\AccountController@provision'); + Route::prefix('accounts')->controller(AdminAccountController::class)->group(function () { + Route::get('{id}/activate', 'activate'); + Route::get('{id}/deactivate', 'deactivate'); + Route::get('{id}/provision', 'provision'); - Route::post('accounts/{id}/recover-by-email', 'Api\Admin\AccountController@recoverByEmail'); + Route::post('{id}/recover-by-email', 'recoverByEmail'); - Route::post('accounts', 'Api\Admin\AccountController@store'); - Route::put('accounts/{id}', 'Api\Admin\AccountController@update'); - Route::get('accounts', 'Api\Admin\AccountController@index'); - Route::get('accounts/{id}', 'Api\Admin\AccountController@show'); - Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy'); - Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search'); - Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail'); + Route::post('/', 'store'); + Route::put('{id}', 'update'); + Route::get('/', 'index'); + Route::get('{id}', 'show'); + Route::delete('{id}', 'destroy'); + Route::get('{sip}/search', 'search'); + Route::get('{email}/search-by-email', 'searchByEmail'); - // Account actions - Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index'); - Route::get('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@show'); - Route::post('accounts/{id}/actions', 'Api\Admin\AccountActionController@store'); - Route::delete('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@destroy'); - Route::put('accounts/{id}/actions/{action_id}', 'Api\Admin\AccountActionController@update'); + Route::post('{id}/types/{type_id}', 'typeAdd'); + Route::delete('{id}/types/{type_id}', 'typeRemove'); + + Route::post('{id}/contacts_lists/{contacts_list_id}', 'contactsListAdd'); + Route::delete('{id}/contacts_lists/{contacts_list_id}', 'contactsListRemove'); + }); // Account contacts - Route::get('accounts/{id}/contacts', 'Api\Admin\AccountContactController@index'); - Route::get('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@show'); - Route::post('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@add'); - Route::delete('accounts/{id}/contacts/{contact_id}', 'Api\Admin\AccountContactController@remove'); + Route::prefix('accounts/{id}/contacts')->controller(AccountContactController::class)->group(function () { + Route::get('/', 'index'); + Route::get('{contact_id}', 'show'); + Route::post('{contact_id}', 'add'); + Route::delete('{contact_id}', 'remove'); + }); - // Account types - Route::get('account_types', 'Api\Admin\AccountTypeController@index'); - Route::get('account_types/{id}', 'Api\Admin\AccountTypeController@show'); - Route::post('account_types', 'Api\Admin\AccountTypeController@store'); - Route::delete('account_types/{id}', 'Api\Admin\AccountTypeController@destroy'); - Route::put('account_types/{id}', 'Api\Admin\AccountTypeController@update'); + Route::apiResource('accounts/{id}/actions', AccountActionController::class); + Route::apiResource('account_types', AccountTypeController::class); - Route::post('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeAdd'); - Route::delete('accounts/{id}/types/{type_id}', 'Api\Admin\AccountController@typeRemove'); + Route::apiResource('contacts_lists', ContactsListController::class); + Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () { + Route::post('{id}/contacts/{contacts_id}', 'contactAdd'); + Route::delete('{id}/contacts/{contacts_id}', 'contactRemove'); + }); }); }); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index b9db97a..2dbeabc 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -129,7 +129,6 @@ if (config('app.web_panel')) { Route::get('auth_tokens/qrcode/{token}', 'Account\AuthTokenController@qrcode')->name('auth_tokens.qrcode'); Route::get('auth_tokens/auth/{token}', 'Account\AuthTokenController@auth')->name('auth_tokens.auth'); - //Route::get('admin/accounts/{acc}/cl/{$cl}/detach', 'Admin\AccountController@detachContactsList')->name('admin.account.contacts_lists.detach2'); Route::name('admin.')->prefix('admin')->middleware(['auth.admin'])->group(function () { // Statistics @@ -155,8 +154,8 @@ if (config('app.web_panel')) { Route::get('/', 'index')->name('index'); Route::post('search', 'search')->name('search'); - Route::get('{account_id}/contacts_lists/detach', 'detachContactsList')->name('contacts_lists.detach'); - Route::post('{account_id}/contacts_lists', 'attachContactsList')->name('contacts_lists.attach'); + Route::get('{account_id}/contacts_lists/detach', 'contactsListRemove')->name('contacts_lists.detach'); + Route::post('{account_id}/contacts_lists', 'contactsListAdd')->name('contacts_lists.attach'); }); Route::name('type.')->prefix('types')->controller(AccountTypeController::class)->group(function () { diff --git a/flexiapi/tests/Feature/ApiAccountContactsTest.php b/flexiapi/tests/Feature/ApiAccountContactsTest.php index b0126f5..9e8fd86 100644 --- a/flexiapi/tests/Feature/ApiAccountContactsTest.php +++ b/flexiapi/tests/Feature/ApiAccountContactsTest.php @@ -32,6 +32,7 @@ class ApiAccountContactTest extends TestCase use RefreshDatabase; protected $route = '/api/accounts'; + protected $contactsListsRoute = '/api/contacts_lists'; protected $method = 'POST'; public function testCreate() @@ -155,11 +156,50 @@ class ApiAccountContactTest extends TestCase * */ - // This will need to be done through the API - $contactList = ContactsList::factory()->create(); - $contactList->contacts()->attach([$password1->account->id, $password2->account->id, $password3->account->id]); + // Create the Contacts list + $contactsListsTitle = 'Contacts List title'; - $admin->account->contactsLists()->attach([$contactList->id]); + $this->keyAuthenticated($admin->account) + ->json($this->method, $this->contactsListsRoute, [ + 'title' => $contactsListsTitle, + 'description' => 'Description' + ]) + ->assertStatus(201); + + $this->assertDatabaseHas('contacts_lists', [ + 'title' => $contactsListsTitle + ]); + + // Attach the Contacts and the Contacts List + + $contactsList = ContactsList::first(); + + $this->keyAuthenticated($admin->account) + ->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password1->account->id) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password2->account->id) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/' . $password3->account->id) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->post($this->contactsListsRoute . '/' . $contactsList->id . '/contacts/1234') + ->assertStatus(404); + + + $this->keyAuthenticated($admin->account) + ->post($this->route . '/' . $admin->account->id . '/contacts_lists/' . $contactsList->id) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->post($this->route . '/' . $admin->account->id . '/contacts_lists/' . $contactsList->id) + ->assertStatus(403); + + // Get the contacts and vcards $this->keyAuthenticated($admin->account) ->get($this->route . '/me/contacts')