diff --git a/asset-bundles.json b/asset-bundles.json index 43af0cf..8c2047f 100644 --- a/asset-bundles.json +++ b/asset-bundles.json @@ -61,6 +61,22 @@ "userfrosting/js/uf-captcha.js", "uf-tweaks/js/pages/register.js" ] + }, + "js/pages/users": { + "scripts": [ + "uf-tweaks/js/pages/users.js" + ], + "options": { + "sprinkle": { + "onCollision": "merge" + } + } + }, + "js/pages/deleted-users": { + "scripts": [ + "uf-tweaks/js/widgets/deleted-users.js", + "uf-tweaks/js/pages/deleted-users.js" + ] } } } diff --git a/assets/uf-tweaks/js/pages/deleted-users.js b/assets/uf-tweaks/js/pages/deleted-users.js new file mode 100644 index 0000000..f153a18 --- /dev/null +++ b/assets/uf-tweaks/js/pages/deleted-users.js @@ -0,0 +1,32 @@ + +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/deleted-users.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /deleted-users + */ + +$(document).ready(function() { + /** + * Navigation buttons + */ + // Return from the deleted organisations page + $("#widget-deletedUsers").find('.js-deletedUsers-return').click(function(e) { + e.preventDefault(); + + window.location.href = site.uri.public + '/users'; + }); + + // Deleted users table + $("#widget-deletedUsers").ufTable({ + dataUrl: site.uri.public + "/api/users/deleted", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind table buttons + $("#widget-deletedUsers").on("pagerComplete.ufTable", function () { + bindDeletedUsersButtons($(this)); + }); +}); diff --git a/assets/uf-tweaks/js/pages/users.js b/assets/uf-tweaks/js/pages/users.js new file mode 100644 index 0000000..798468f --- /dev/null +++ b/assets/uf-tweaks/js/pages/users.js @@ -0,0 +1,20 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/users.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /users + */ + +$(document).ready(function() { + /** + * Navigation buttons + */ + // View the deleted organisations page + $("#widget-users").find('.js-user-viewDeleted').click(function(e) { + e.preventDefault(); + + window.location.href = site.uri.public + '/users/deleted'; + }); +}); diff --git a/assets/uf-tweaks/js/widgets/deleted-users.js b/assets/uf-tweaks/js/widgets/deleted-users.js new file mode 100644 index 0000000..7712585 --- /dev/null +++ b/assets/uf-tweaks/js/widgets/deleted-users.js @@ -0,0 +1,82 @@ +/** + * Users widget. Sets up dropdowns, modals, etc for a table of deleted users. + */ + +/** + * Link delete user action buttons, for example in a table or on a specific users's page. + * @param {module:jQuery} el jQuery wrapped element to target. + * @param {{delete_redirect: string}} options Options used to modify behaviour of button actions. + */ +function bindDeletedUsersButtons(el, options) { + if (!options) options = {}; + + /** + * Buttons that launch a modal dialog + */ + + // Permenetly delete user button + el.find('.js-user-permanentDelete').click(function(e) { + e.preventDefault(); + + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/confirm-permanent-delete", + ajaxParams: { + user_name: $(this).data('user_name') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function() { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Navigate or reload page on success + if (options.delete_redirect) window.location.href = options.delete_redirect; + else window.location.reload(); + }); + }); + }); + + // Restore user button + el.find('.js-user-restore').click(function(e) { + e.preventDefault(); + + var data = {}; + data[site.csrf.keys.name] = site.csrf.name; + data[site.csrf.keys.value] = site.csrf.value; + + var url = site.uri.public + '/api/users/u/' + $(this).data('user_name') + '/restore'; + var debugAjax = (typeof site !== "undefined") && site.debug.ajax; + + return $.ajax({ + type: "POST", + url: url, + data: data, + dataType: debugAjax ? 'html' : 'json', + }).fail(function(jqXHR) { + // Error messages + if (debugAjax && jqXHR.responseText) { + document.write(jqXHR.responseText); + document.close(); + } else { + console.log("Error (" + jqXHR.status + "): " + jqXHR.responseText); + + // Display errors on failure + // TODO: ufAlerts widget should have a 'destroy' method + if (!$("#alerts-page").data('ufAlerts')) { + $("#alerts-page").ufAlerts(); + } else { + $("#alerts-page").ufAlerts('clear'); + } + + $("#alerts-page").ufAlerts('fetch').ufAlerts('render'); + } + + return jqXHR; + }).done(function(response) { + window.location.reload(); + }); + }); +} diff --git a/config/default.php b/config/default.php index 33cb144..285036e 100644 --- a/config/default.php +++ b/config/default.php @@ -8,9 +8,16 @@ */ return [ + /* + * ---------------------------------------------------------------------- + * Debug Configuration + * ---------------------------------------------------------------------- + * Turn any of those on to help debug your app + */ 'debug' => [ 'tokens' => false, ], + /* * ---------------------------------------------------------------------- * Database Config @@ -33,6 +40,18 @@ return [ 'minutes' => 540, ], + /* + * ---------------------------------------------------------------------- + * Users Config + * ---------------------------------------------------------------------- + * Allow permanent deletion of users. Default to false. + * Installers must turn this on to change default behaviour. + */ + 'users' => [ + 'allow_permanent_delete' => false, + 'purge_activities' => false, + ], + /* * ---------------------------------------------------------------------- * PHP global settings diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index b758190..6d4d20a 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -8,5 +8,21 @@ */ return [ - 'ICON' => 'Icon', + 'USER' => [ + 'VIEW_DELETED' => 'View deleted users', + 'DELETED_PAGE_DESCRIPTION' => 'A listing of the deleted users for your site. Provides management tools for restoring and permenently deleting users.', + + 'RESTORE_DELETED' => 'Restore user', + 'RESTORE_SUCCESSFUL' => 'Successfully restored user {{user_name}}', + + 'PERMANENT_DELETE' => 'Permanetly delete user', + 'PERMANENT_DELETE_CONFIRM' => 'Are you sure you want to permanently delete the user {{user_name}}?', + 'PERMANENT_DELETE_YES' => 'Yes, permanently delete user', + 'PERMANENT_DELETION_SUCCESSFUL' => 'User {{user_name}} has been permanently deleted.', + 'PERMANENT_DELETE_DISABLED' => 'Permanent deletion of users is currently disabled via the configuration.', + ], + + 'DELETED' => 'Deleted', + 'ICON' => 'Icon', + 'RETURN' => 'Return', ]; \ No newline at end of file diff --git a/routes/users.php b/routes/users.php new file mode 100644 index 0000000..7dd7c31 --- /dev/null +++ b/routes/users.php @@ -0,0 +1,29 @@ +group('/users', function () { + $this->get('/deleted', 'UserFrosting\Sprinkle\UFTweaks\Controller\DeletedUserController:pageListDeleted'); +})->add('authGuard')->add(new NoCache()); + +$app->group('/api/users', function () { + $this->get('/deleted', 'UserFrosting\Sprinkle\UFTweaks\Controller\DeletedUserController:getListDeleted'); + + $this->post('/u/{user_name}/restore', 'UserFrosting\Sprinkle\UFTweaks\Controller\DeletedUserController:restore'); + + $this->delete('/u/{user_name}/permanent', 'UserFrosting\Sprinkle\UFTweaks\Controller\DeletedUserController:deletePermanent'); +})->add('authGuard')->add(new NoCache()); + +$app->group('/modals/users', function () { + $this->get('/confirm-permanent-delete', 'UserFrosting\Sprinkle\UFTweaks\Controller\DeletedUserController:getModalConfirmPermanentDelete'); +})->add('authGuard')->add(new NoCache()); diff --git a/src/Controller/DeletedUserController.php b/src/Controller/DeletedUserController.php new file mode 100644 index 0000000..202e7da --- /dev/null +++ b/src/Controller/DeletedUserController.php @@ -0,0 +1,350 @@ +ci->authorizer; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + + $user = $this->getDeletedUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException(); + } + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'permanently_delete_user', [ + 'user' => $user, + ])) { + throw new ForbiddenException(); + } + + // Config-controlled page + if (!$config['users.allow_permanent_delete']) { + $ms->addMessageTranslated('danger', 'USER.PERMANENT_DELETE_DISABLED'); + return $response->withJson([], 400); + } + + $user_name = $user->user_name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($user, $user_name, $currentUser, $config) { + $user->forceDelete(); + unset($user); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} permenetly deleted user {$user_name}.", [ + 'type' => 'user_delete', + 'user_id' => $currentUser->id, + ]); + }); + + $ms->addMessageTranslated('success', 'USER.PERMANENT_DELETION_SUCCESSFUL', [ + 'user_name' => $user_name, + ]); + + return $response->withJson([], 200); + } + + /** + * Restores a deleted user + * + * Before doing so, checks that: + * 1. The current user has permission to restore the requested user; + * 2. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * + * Request type: PUT + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If user is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function restore(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $user = $this->getDeletedUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException(); + } + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'restore_deleted_user', [ + 'user' => $user, + ])) { + throw new ForbiddenException(); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($user, $currentUser) { + $user->restore(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} restored deleted user {$user->user_name}.", [ + 'type' => 'user_restore', + 'user_id' => $currentUser->id, + ]); + }); + + $ms->addMessageTranslated('success', 'USER.RESTORE_SUCCESSFUL', [ + 'user_name' => $user->user_name, + ]); + + return $response->withJson([], 200); + } + + + /** + * Returns a list of deleted users. + * + * Generates a list of deleted users, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws ForbiddenException If user is not authorized to access page + */ + public function getListDeleted(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_deleted_users')) { + throw new ForbiddenException(); + } + + // GET parameters + $params = $request->getQueryParams(); + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) { + return $query->onlyTrashed(); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + + /** + * Get permanent deletion confirmation modal. + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If user is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function getModalConfirmPermanentDelete(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getDeletedUserFromParams($params); + + // If the user doesn't exist, return not found + if (!$user) { + throw new NotFoundException(); + } + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'permanently_delete_user', [ + 'user' => $user, + ])) { + throw new ForbiddenException(); + } + + // Config-controlled page + if (!$config['users.allow_permanent_delete']) { + $exception = new BadRequestException(); + $exception->addUserMessage('USER.PERMANENT_DELETE_DISABLED'); + throw $exception; + } + + return $this->ci->view->render($response, 'modals/confirm-permanently-delete-user.html.twig', [ + 'user' => $user, + 'form' => [ + 'action' => "api/users/u/{$user->user_name}/permanent", + ], + ]); + } + + + /** + * Renders the user listing page for deleted users. + * + * This page renders a table of delete users, with dropdown menus for admin actions for each user. + * Actions typically include: restore user, permanently delete etc. + * This page requires authentication. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws ForbiddenException If user is not authorized to access page + */ + public function pageListDeleted(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_deleted_users')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/deleted-users.html.twig'); + } + + + /** + * Get User instance from params. + * + * @param string[] $params + * + * @throws BadRequestException + * + * @return User|null + */ + protected function getDeletedUserFromParams(array $params) + { + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/get-by-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach ($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + + throw $e; + } + + // Query to get the user + $query = $classMapper->getClassMapping('user')::where('user_name', $data['user_name'])->withTrashed(); + + // Run the query + $user = $query->first(); + + return $user; + } +} diff --git a/src/Database/Migrations/v1_0_0/UpdateActivitiesTable.php b/src/Database/Migrations/v1_0_0/UpdateActivitiesTable.php new file mode 100644 index 0000000..de0e922 --- /dev/null +++ b/src/Database/Migrations/v1_0_0/UpdateActivitiesTable.php @@ -0,0 +1,54 @@ +schema->hasTable('activities')) { + $this->schema->table('activities', function (Blueprint $table) { + $table->integer('user_id')->unsigned()->nullable()->change(); + }); + } + } + + /** + * {@inheritdoc} + */ + public function down() + { + if ($this->schema->hasTable('activities')) { + $this->schema->table('activities', function (Blueprint $table) { + $table->integer('user_id')->unsigned()->change(); + }); + } + } +} diff --git a/src/Database/ModelTraits/UserPermanentlyDeletable.php b/src/Database/ModelTraits/UserPermanentlyDeletable.php new file mode 100644 index 0000000..3ab6bab --- /dev/null +++ b/src/Database/ModelTraits/UserPermanentlyDeletable.php @@ -0,0 +1,42 @@ +classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = static::$ci->config; + + if ($this->isForceDeleting() && !$config['users.purge_activities']) { + // Keep the activity log, but lose the user field + $classMapper->getClassMapping('activity')::query() + ->where('user_id', $this->id) + ->update(['user_id' => null]); + + $this->refresh(); + } + + return true; + } + + public function bootUserPermanentlyDeletable() + { + static::deleting(function ($user) { + if (!$user->preventActivityPurge()) { + return false; + } + + return true; + }); + } +} \ No newline at end of file diff --git a/src/Database/Models/User.php b/src/Database/Models/User.php new file mode 100644 index 0000000..24dd685 --- /dev/null +++ b/src/Database/Models/User.php @@ -0,0 +1,85 @@ +classMapper; + + // Remove all role associations + $this->roles()->detach(); + + // Remove last activity association + $this->lastActivity()->dissociate(); + $this->save(); + + // Remove all user tokens + $this->activities()->delete(); + $this->passwordResets()->delete(); + $classMapper->getClassMapping('verification')::where('user_id', $this->id)->delete(); + $classMapper->getClassMapping('persistence')::where('user_id', $this->id)->delete(); + + return true; + } + + /** + * Delete this user from the database, along with any linked roles and activities. + * + * @param bool $hardDelete Trigger a forceDelete() + * + * @return bool true if the deletion was successful, false otherwise. + */ + public function delete($hardDelete = false) + { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + if ($hardDelete) { + return $this->forceDelete(); + } + + if ($this->isForceDeleting()) { + if (!$config['users.allow_permanent_delete']) { + throw new LogicException('Permanent deletion is disabled.'); + } + + if ($this->fireModelEvent('deleting') === false) { + return false; + } + + $this->deleteUserAssociations(); + } + + $result = parent::delete(); + + return $result; + } +} diff --git a/src/Database/Seeds/DefaultPermissions.php b/src/Database/Seeds/DefaultPermissions.php index 742fbaf..c04eed4 100644 --- a/src/Database/Seeds/DefaultPermissions.php +++ b/src/Database/Seeds/DefaultPermissions.php @@ -40,7 +40,7 @@ class DefaultPermissions extends UFDefaultPermissions */ protected function getPermissions() { - $base_permissions = parent::getPermissions(); + $basePermissions = parent::getPermissions(); $defaultRoleIds = [ 'user' => Role::where('slug', 'user')->first()->id, @@ -48,66 +48,84 @@ class DefaultPermissions extends UFDefaultPermissions 'site-admin' => Role::where('slug', 'site-admin')->first()->id, ]; - return array_merge( - $base_permissions, - [ - 'uri_role' => new Permission([ - 'slug' => 'uri_role', - 'name' => 'View role', - 'conditions' => 'always()', - 'description' => 'View the role page of any role.', - ]), - 'uri_roles' => new Permission([ - 'slug' => 'uri_roles', - 'name' => 'Role management page', - 'conditions' => 'always()', - 'description' => 'View a page containing a table of roles.', - ]), - 'uri_permission' => new Permission([ - 'slug' => 'uri_permission', - 'name' => 'View permission', - 'conditions' => 'always()', - 'description' => 'View the permission page of any permission.', - ]), - 'uri_permissions' => new Permission([ - 'slug' => 'uri_permissions', - 'name' => 'Permission management page', - 'conditions' => 'always()', - 'description' => 'View a page containing a table of permissions.', - ]), + $newPermissions = [ + 'uri_role' => new Permission([ + 'slug' => 'uri_role', + 'name' => 'View role', + 'conditions' => 'always()', + 'description' => 'View the role page of any role.', + ]), + 'uri_roles' => new Permission([ + 'slug' => 'uri_roles', + 'name' => 'Role management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of roles.', + ]), + 'uri_permission' => new Permission([ + 'slug' => 'uri_permission', + 'name' => 'View permission', + 'conditions' => 'always()', + 'description' => 'View the permission page of any permission.', + ]), + 'uri_permissions' => new Permission([ + 'slug' => 'uri_permissions', + 'name' => 'Permission management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of permissions.', + ]), - 'create_role' => new Permission([ - 'slug' => 'create_role', - 'name' => 'Create role', - 'conditions' => 'always()', - 'description' => 'Create a new role.', - ]), - 'view_role_field' => new Permission([ - 'slug' => 'view_role_field', - 'name' => 'View role', - 'conditions' => "in(property,['slug','name','description','permissions','users'])", - 'description' => 'View certain properties of any role.', - ]), - 'update_role_field' => new Permission([ - 'slug' => 'update_role_field', - 'name' => 'Edit role', - 'conditions' => "is_master(self.id) || subset(fields,['slug','name','description'])", - 'description' => 'Edit basic properties of any role.', - ]), - 'update_role_permissions' => new Permission([ - 'slug' => 'update_role_permissions', - 'name' => 'Edit role permissions', - 'conditions' => "is_master(self.id) || subset(fields,['permissions'])", - 'description' => 'Edit permissions of any role.', - ]), - 'delete_role' => new Permission([ - 'slug' => 'delete_role', - 'name' => 'Delete role', - 'conditions' => 'always()', - 'description' => 'Delete a role.', - ]), - ] - ); + 'create_role' => new Permission([ + 'slug' => 'create_role', + 'name' => 'Create role', + 'conditions' => 'always()', + 'description' => 'Create a new role.', + ]), + 'view_role_field' => new Permission([ + 'slug' => 'view_role_field', + 'name' => 'View role', + 'conditions' => "in(property,['slug','name','description','permissions','users'])", + 'description' => 'View certain properties of any role.', + ]), + 'update_role_field' => new Permission([ + 'slug' => 'update_role_field', + 'name' => 'Edit role', + 'conditions' => "is_master(self.id) || subset(fields,['slug','name','description'])", + 'description' => 'Edit basic properties of any role.', + ]), + 'update_role_permissions' => new Permission([ + 'slug' => 'update_role_permissions', + 'name' => 'Edit role permissions', + 'conditions' => "is_master(self.id) || subset(fields,['permissions'])", + 'description' => 'Edit permissions of any role.', + ]), + 'delete_role' => new Permission([ + 'slug' => 'delete_role', + 'name' => 'Delete role', + 'conditions' => 'always()', + 'description' => 'Delete a role.', + ]), + + 'permanently_delete_user' => new Permission([ + 'slug' => 'permanently_delete_user', + 'name' => 'Permanently delete user', + 'conditions' => 'always()', + 'description' => 'Provides the permission to permanently delete users.', + ]), + 'restore_deleted_user' => new Permission([ + 'slug' => 'restore_deleted_user', + 'name' => 'Restore deleted user', + 'conditions' => "always()", + 'description' => 'Provides the permission to restore deleted users.', + ]), + 'uri_deleted_users' => new Permission([ + 'slug' => 'uri_deleted_users', + 'name' => 'Deleted users management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a list of deleted users.', + ]), + ]; + + return array_merge($basePermissions, $newPermissions); } /** @@ -150,6 +168,10 @@ class DefaultPermissions extends UFDefaultPermissions $permissions['uri_permissions']->id, $permissions['view_role_field']->id, + + $permissions['permanently_delete_user']->id, + $permissions['restore_deleted_user']->id, + $permissions['uri_deleted_users']->id, ]); } diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index ac1fb44..dbc5bf7 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -42,6 +42,7 @@ class ServicesProvider */ $container->extend('classMapper', function ($classMapper, $c) { $classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\UFTweaks\Sprunje\ActivitySprunje'); + $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\UFTweaks\Database\Models\User'); return $classMapper; }); diff --git a/templates/modals/confirm-permanently-delete-user.html.twig b/templates/modals/confirm-permanently-delete-user.html.twig new file mode 100644 index 0000000..a3898fc --- /dev/null +++ b/templates/modals/confirm-permanently-delete-user.html.twig @@ -0,0 +1,19 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("USER.PERMANENT_DELETE")}}{% endblock %} + +{% block modal_body %} +
+ {% include "forms/csrf.html.twig" %} +
+
+

{{translate("USER.PERMANENT_DELETE_CONFIRM", {user_name: user.user_name})}}

+
+ {{translate("ACTION_CANNOT_UNDONE")}} +
+
+ + +
+
+{% endblock %} diff --git a/templates/pages/deleted-users.html.twig b/templates/pages/deleted-users.html.twig new file mode 100644 index 0000000..9d10bf2 --- /dev/null +++ b/templates/pages/deleted-users.html.twig @@ -0,0 +1,47 @@ +{% extends "pages/users.html.twig" %} + +{% block page_title %}{{ translate("DELETED") }} {{ translate("USER", 2) }}{% endblock %} + +{% block page_description %}{{ translate("USER.DELETED_PAGE_DESCRIPTION") }}{% endblock %} + +{% block body_matter %} +
+
+
+
+

{{ translate("DELETED") }} {{translate('USER', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/deleted-users.html.twig" with { + "table" : { + "id" : "table-deletedUsers", + "columns" : [ + (checkAccess('view_user_field', { "property" : 'activities' }) ? "last_activity" : "") + ] + } + } + %} +
+ +
+
+
+{% endblock %} +{% block scripts_page %} + + + + + {{ assets.js('js/form-widgets') | raw }} + + + {{ assets.js('js/pages/deleted-users') | raw }} + +{% endblock %} diff --git a/templates/pages/users.html.twig b/templates/pages/users.html.twig index 3f3265b..e67a0d0 100644 --- a/templates/pages/users.html.twig +++ b/templates/pages/users.html.twig @@ -19,14 +19,19 @@ } %} - {% if checkAccess('create_user') %} - {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/tables/columns/deleted_users-actions.html.twig b/templates/tables/columns/deleted_users-actions.html.twig new file mode 100644 index 0000000..82beac5 --- /dev/null +++ b/templates/tables/columns/deleted_users-actions.html.twig @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/templates/tables/deleted-users.html.twig b/templates/tables/deleted-users.html.twig new file mode 100644 index 0000000..2b0c611 --- /dev/null +++ b/templates/tables/deleted-users.html.twig @@ -0,0 +1,5 @@ +{% extends "@uf-tweaks/tables/users.html.twig" %} + +{% block table_users_column_actions %} + {% include "tables/columns/deleted_users-actions.html.twig" %} +{% endblock %} \ No newline at end of file