Incorporate the permanent user deletion sprinkle

This commit is contained in:
2023-07-25 14:21:42 +01:00
parent 243987382a
commit 50b8e02839
18 changed files with 934 additions and 66 deletions

View File

@@ -61,6 +61,22 @@
"userfrosting/js/uf-captcha.js", "userfrosting/js/uf-captcha.js",
"uf-tweaks/js/pages/register.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"
]
} }
} }
} }

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,16 @@
*/ */
return [ return [
/*
* ----------------------------------------------------------------------
* Debug Configuration
* ----------------------------------------------------------------------
* Turn any of those on to help debug your app
*/
'debug' => [ 'debug' => [
'tokens' => false, 'tokens' => false,
], ],
/* /*
* ---------------------------------------------------------------------- * ----------------------------------------------------------------------
* Database Config * Database Config
@@ -33,6 +40,18 @@ return [
'minutes' => 540, '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 * PHP global settings

View File

@@ -8,5 +8,21 @@
*/ */
return [ 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 <strong>{{user_name}}</strong>',
'PERMANENT_DELETE' => 'Permanetly delete user',
'PERMANENT_DELETE_CONFIRM' => 'Are you sure you want to <strong>permanently</strong> delete the user <strong>{{user_name}}</strong>?',
'PERMANENT_DELETE_YES' => 'Yes, permanently delete user',
'PERMANENT_DELETION_SUCCESSFUL' => 'User <strong>{{user_name}}</strong> has been permanently deleted.',
'PERMANENT_DELETE_DISABLED' => 'Permanent deletion of users is currently disabled via the configuration.',
],
'DELETED' => 'Deleted',
'ICON' => 'Icon',
'RETURN' => 'Return',
]; ];

29
routes/users.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
/*
* AVSDev UF Tweaks (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks
* @license https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks/blob/master/LICENSE.md (LGPL-3.0 License)
*/
use UserFrosting\Sprinkle\Core\Util\NoCache;
/*
* Routes for administrative user management.
*/
$app->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());

View File

@@ -0,0 +1,350 @@
<?php
/*
* AVSDev UF Tweaks (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks
* @license https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\UFTweaks\Controller;
use Illuminate\Database\Capsule\Manager as Capsule;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use UserFrosting\Fortress\RequestDataTransformer;
use UserFrosting\Fortress\RequestSchema;
use UserFrosting\Fortress\ServerSideValidator;
use UserFrosting\Sprinkle\Core\Controller\SimpleController;
use UserFrosting\Support\Exception\BadRequestException;
use UserFrosting\Support\Exception\ForbiddenException;
use UserFrosting\Support\Exception\NotFoundException;
/**
* Controller class for deleted user related requests, including restoring and permanently deleting users.
*
* @author Craig Williams (https://avsdev.uk)
*/
class DeletedUserController extends SimpleController
{
/**
* Processes the request to permanently delete a deleted user.
*
* Permanently deletes the specified user.
* Before doing so, checks that:
* 1. The current user has permission to delete 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: DELETE
*
* @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 deletePermanent(Request $request, Response $response, $args)
{
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
$authorizer = $this->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;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* AVSDev UF Tweaks (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks
* @license https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\UFTweaks\Database\Migrations\v1_0_0;
use Illuminate\Database\Schema\Blueprint;
use UserFrosting\Sprinkle\Core\Database\Migration;
/**
* Users table migration
* Changes `user_id` column properties to nullable to allow users to be deleted without clearing the audit log for that user.
* Version 1.0.0.
*
* @author Craig Williams (https://avsdev.uk)
*/
class UpdateActivitiesTable extends Migration
{
/**
* {@inheritdoc}
*/
public static $dependencies = [
'UserFrosting\Sprinkle\Account\Database\Migrations\v400\ActivitiesTable',
];
/**
* {@inheritdoc}
*/
public function up()
{
if ($this->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();
});
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* AVSDev UF Tweaks (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks
* @license https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\UFTweaks\Database\ModelTraits;
trait UserPermanentlyDeletable {
protected function preventActivityPurge() {
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = static::$ci->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;
});
}
}

View File

@@ -0,0 +1,85 @@
<?php
/*
* AVSDev UF Tweaks (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks
* @license https://git.avsdev.uk/avsdev/sprinkle-uf-tweaks/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\UFTweaks\Database\Models;
use LogicException;
use UserFrosting\Sprinkle\Account\Database\Models\User as UFUser;
use UserFrosting\Sprinkle\UFTweaks\Database\ModelTraits\UserPermanentlyDeletable;
/**
* User Class.
*
* Extends the UF User object by adding the UserPermanentlyDeletable trait
*
* @author Craig Williams (https://avsdev.uk)
*/
class User extends UFUser
{
use UserPermanentlyDeletable;
/**
* Delete this user from the database, along with any linked roles and activities.
*
* @return bool true if the deletion was successful, false otherwise.
*/
protected function deleteUserAssociations()
{
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = static::$ci->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;
}
}

View File

@@ -40,7 +40,7 @@ class DefaultPermissions extends UFDefaultPermissions
*/ */
protected function getPermissions() protected function getPermissions()
{ {
$base_permissions = parent::getPermissions(); $basePermissions = parent::getPermissions();
$defaultRoleIds = [ $defaultRoleIds = [
'user' => Role::where('slug', 'user')->first()->id, 'user' => Role::where('slug', 'user')->first()->id,
@@ -48,66 +48,84 @@ class DefaultPermissions extends UFDefaultPermissions
'site-admin' => Role::where('slug', 'site-admin')->first()->id, 'site-admin' => Role::where('slug', 'site-admin')->first()->id,
]; ];
return array_merge( $newPermissions = [
$base_permissions, 'uri_role' => new Permission([
[ 'slug' => 'uri_role',
'uri_role' => new Permission([ 'name' => 'View role',
'slug' => 'uri_role', 'conditions' => 'always()',
'name' => 'View role', 'description' => 'View the role page of any role.',
'conditions' => 'always()', ]),
'description' => 'View the role page of any role.', 'uri_roles' => new Permission([
]), 'slug' => 'uri_roles',
'uri_roles' => new Permission([ 'name' => 'Role management page',
'slug' => 'uri_roles', 'conditions' => 'always()',
'name' => 'Role management page', 'description' => 'View a page containing a table of roles.',
'conditions' => 'always()', ]),
'description' => 'View a page containing a table of roles.', 'uri_permission' => new Permission([
]), 'slug' => 'uri_permission',
'uri_permission' => new Permission([ 'name' => 'View permission',
'slug' => 'uri_permission', 'conditions' => 'always()',
'name' => 'View permission', 'description' => 'View the permission page of any permission.',
'conditions' => 'always()', ]),
'description' => 'View the permission page of any permission.', 'uri_permissions' => new Permission([
]), 'slug' => 'uri_permissions',
'uri_permissions' => new Permission([ 'name' => 'Permission management page',
'slug' => 'uri_permissions', 'conditions' => 'always()',
'name' => 'Permission management page', 'description' => 'View a page containing a table of permissions.',
'conditions' => 'always()', ]),
'description' => 'View a page containing a table of permissions.',
]),
'create_role' => new Permission([ 'create_role' => new Permission([
'slug' => 'create_role', 'slug' => 'create_role',
'name' => 'Create role', 'name' => 'Create role',
'conditions' => 'always()', 'conditions' => 'always()',
'description' => 'Create a new role.', 'description' => 'Create a new role.',
]), ]),
'view_role_field' => new Permission([ 'view_role_field' => new Permission([
'slug' => 'view_role_field', 'slug' => 'view_role_field',
'name' => 'View role', 'name' => 'View role',
'conditions' => "in(property,['slug','name','description','permissions','users'])", 'conditions' => "in(property,['slug','name','description','permissions','users'])",
'description' => 'View certain properties of any role.', 'description' => 'View certain properties of any role.',
]), ]),
'update_role_field' => new Permission([ 'update_role_field' => new Permission([
'slug' => 'update_role_field', 'slug' => 'update_role_field',
'name' => 'Edit role', 'name' => 'Edit role',
'conditions' => "is_master(self.id) || subset(fields,['slug','name','description'])", 'conditions' => "is_master(self.id) || subset(fields,['slug','name','description'])",
'description' => 'Edit basic properties of any role.', 'description' => 'Edit basic properties of any role.',
]), ]),
'update_role_permissions' => new Permission([ 'update_role_permissions' => new Permission([
'slug' => 'update_role_permissions', 'slug' => 'update_role_permissions',
'name' => 'Edit role permissions', 'name' => 'Edit role permissions',
'conditions' => "is_master(self.id) || subset(fields,['permissions'])", 'conditions' => "is_master(self.id) || subset(fields,['permissions'])",
'description' => 'Edit permissions of any role.', 'description' => 'Edit permissions of any role.',
]), ]),
'delete_role' => new Permission([ 'delete_role' => new Permission([
'slug' => 'delete_role', 'slug' => 'delete_role',
'name' => 'Delete role', 'name' => 'Delete role',
'conditions' => 'always()', 'conditions' => 'always()',
'description' => 'Delete a role.', '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['uri_permissions']->id,
$permissions['view_role_field']->id, $permissions['view_role_field']->id,
$permissions['permanently_delete_user']->id,
$permissions['restore_deleted_user']->id,
$permissions['uri_deleted_users']->id,
]); ]);
} }

View File

@@ -42,6 +42,7 @@ class ServicesProvider
*/ */
$container->extend('classMapper', function ($classMapper, $c) { $container->extend('classMapper', function ($classMapper, $c) {
$classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\UFTweaks\Sprunje\ActivitySprunje'); $classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\UFTweaks\Sprunje\ActivitySprunje');
$classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\UFTweaks\Database\Models\User');
return $classMapper; return $classMapper;
}); });

View File

@@ -0,0 +1,19 @@
{% extends "modals/modal.html.twig" %}
{% block modal_title %}{{translate("USER.PERMANENT_DELETE")}}{% endblock %}
{% block modal_body %}
<form class="js-form" method="delete" action="{{site.uri.public}}/{{form.action}}">
{% include "forms/csrf.html.twig" %}
<div class="js-form-alerts">
</div>
<h4>{{translate("USER.PERMANENT_DELETE_CONFIRM", {user_name: user.user_name})}}</h4>
<br>
<small>{{translate("ACTION_CANNOT_UNDONE")}}</small>
<br>
<div class="btn-group-action">
<button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("USER.PERMANENT_DELETE_YES")}}</button>
<button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button>
</div>
</form>
{% endblock %}

View File

@@ -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 %}
<div class="row">
<div class="col-md-12">
<div id="widget-deletedUsers" class="box box-primary">
<div class="box-header">
<h3 class="box-title"><i class="fas fa-fw fa-sitemap"></i> {{ translate("DELETED") }} {{translate('USER', 2)}}</h3>
{% include "tables/table-tool-menu.html.twig" %}
</div>
<div class="box-body">
{% include "tables/deleted-users.html.twig" with {
"table" : {
"id" : "table-deletedUsers",
"columns" : [
(checkAccess('view_user_field', { "property" : 'activities' }) ? "last_activity" : "")
]
}
}
%}
</div>
<div class="box-footer">
<button type="button" class="btn btn-success js-deletedUsers-return">
<i class="fas fa-arrow-left"></i> {{translate("RETURN")}}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts_page %}
<!-- Include validation rules -->
<script>
{% include "pages/partials/page.js.twig" %}
</script>
<!-- Include form widgets JS -->
{{ assets.js('js/form-widgets') | raw }}
<!-- Include page-specific JS -->
{{ assets.js('js/pages/deleted-users') | raw }}
{% endblock %}

View File

@@ -19,13 +19,18 @@
} }
%} %}
</div> </div>
{% if checkAccess('create_user') %}
<div class="box-footer"> <div class="box-footer">
<button type="button" class="btn btn-success js-user-create"> {% if checkAccess('create_user') %}
<i class="fas fa-plus-square"></i> {{ translate("USER.CREATE")}} <button type="button" class="btn btn-success js-user-create">
<i class="fas fa-plus-square"></i> {{ translate("USER.CREATE")}}
</button>
{% endif %}
{% if checkAccess('uri_deleted_users') %}
<button type="button" class="btn btn-danger js-user-viewDeleted">
<i class="fas fa-minus-square"></i> {{ translate("USER.VIEW_DELETED")}}
</button> </button>
{% endif %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,24 @@
<script id="{{table.id}}-column-actions" type="text/x-handlebars-template">
{% verbatim %}
<td class="uf-table-fit-width">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
{% endverbatim %}{{translate("ACTIONS")}}{% verbatim %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li>
<a href="#" data-user_name="{{row.user_name}}" class="js-user-restore">
<i class="fas fa-trash-restore"></i> {% endverbatim %}{{translate("USER.RESTORE_DELETED")}}{% verbatim %}
</a>
</li>
<li>
<a href="#" data-user_name="{{row.user_name}}" class="js-user-permanentDelete">
<i class="fas fa-trash-alt"></i> {% endverbatim %}{{translate("USER.PERMANENT_DELETE")}}{% verbatim %}
</a>
</li>
</ul>
</div>
</td>
{% endverbatim %}
</script>

View File

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