diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/README.md b/README.md index 8b99006..f6450d4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ -# sprinkle-anti-dashboard +# sprinkle-uf-tweaks -Removes the dashboard permission from 'users' role. \ No newline at end of file +Fixes/tweaks a few "issues" with the default UserFrosting installation, including: + +- Removes the uri_dashboard permission from 'users' role +- Adds the uri_dashboard permission to the site-admin and group-admin roles +- Restructures the dashboard to be more role-friendly +- Fixes the authenticator generating 'PHP Notice's (parameter to count() must be an array) +- Don't send the user to the account settings on login, send them to the index instead +- Adds a user creation button to the group admin page +- Fixes showing user activity depending on the current user's permission to view it +- Allow site-admins to view roles & permissions +- Allow site-admins to edit basic role details (name, slug & description) \ No newline at end of file diff --git a/asset-bundles.json b/asset-bundles.json new file mode 100644 index 0000000..209aa16 --- /dev/null +++ b/asset-bundles.json @@ -0,0 +1,14 @@ +{ + "bundle": { + "js/pages/group": { + "scripts": [ + "uf-tweaks/js/pages/group.js" + ], + "options": { + "sprinkle": { + "onCollision": "merge" + } + } + } + } +} diff --git a/assets/uf-tweaks/js/pages/group.js b/assets/uf-tweaks/js/pages/group.js new file mode 100644 index 0000000..211fa35 --- /dev/null +++ b/assets/uf-tweaks/js/pages/group.js @@ -0,0 +1,13 @@ +/** + * 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 uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /groups/g/{slug} + */ + +$(document).ready(function() { + // Bind user creation button + bindUserCreationButton($("#widget-group-users")); +}); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4b6d54f --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "name": "avsdev/sprinkle-uf-tweaks", + "type": "userfrosting-sprinkle", + "description": "Sprinkle to tweak the base UF installation to be tighter.", + "autoload": { + "psr-4": { + "UserFrosting\\Sprinkle\\UFTweaks\\": "src/" + } + } +} \ No newline at end of file diff --git a/config/default.php b/config/default.php new file mode 100644 index 0000000..ba00b65 --- /dev/null +++ b/config/default.php @@ -0,0 +1,45 @@ + [ + 'auth' => true, + ], + /* + * ---------------------------------------------------------------------- + * Database Config + * ---------------------------------------------------------------------- + * Added schema to default with a default schema of public (postgres) + */ + 'db' => [ + 'default' => [ + 'schema' => [env('DB_SCHEMA'), 'public'], + ], + ], + + /* + * ---------------------------------------------------------------------- + * Session Config + * ---------------------------------------------------------------------- + * Extended session timeout to be 9 hours (average working day) for development + */ + 'session' => [ + 'minutes' => 540, + ], + + /* + * ---------------------------------------------------------------------- + * PHP global settings + * ---------------------------------------------------------------------- + * Personal pereference of default timezone... + */ + 'php' => [ + 'timezone' => 'Europe/London', + ], +]; diff --git a/config/production.php b/config/production.php new file mode 100644 index 0000000..45c50f9 --- /dev/null +++ b/config/production.php @@ -0,0 +1,30 @@ + [ + 'minutes' => 60, + ], + + /* + * ---------------------------------------------------------------------- + * PHP global settings + * ---------------------------------------------------------------------- + * Default to php's recommended production value for error_reporting + */ + 'php' => [ + 'error_reporting' => E_ALL & ~E_DEPRECATED & ~E_STRICT, + ], +]; diff --git a/src/Database/Seeds/ClearPermissions.php b/src/Database/Seeds/ClearPermissions.php new file mode 100644 index 0000000..b127fda --- /dev/null +++ b/src/Database/Seeds/ClearPermissions.php @@ -0,0 +1,33 @@ +each(function($perm) { + $perm->roles()->sync([]); + }); + + Permission::whereRaw('1 = 1')->delete(); + } +} diff --git a/src/Database/Seeds/DefaultPermissions.php b/src/Database/Seeds/DefaultPermissions.php new file mode 100644 index 0000000..b308f64 --- /dev/null +++ b/src/Database/Seeds/DefaultPermissions.php @@ -0,0 +1,185 @@ +getPermissions(); + $this->savePermissions($permissions); + + // Add default mappings to permissions + $this->syncPermissionsRole($permissions); + } + + /** + * @return array Permissions to seed + */ + protected function getPermissions() + { + $base_permissions = parent::getPermissions(); + + $defaultRoleIds = [ + 'user' => Role::where('slug', 'user')->first()->id, + 'group-admin' => Role::where('slug', 'group-admin')->first()->id, + '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.', + ]), + + '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_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.', + ]), + 'update_role_permissions_limited' => new Permission([ + 'slug' => 'update_role_permissions', + 'name' => 'Edit role permissions', + 'conditions' => "is_master(self.id) || (!has_role(self.id,role.id) && role.id != {$defaultRoleIds['site-admin']} && subset(fields,['permissions']))", + 'description' => 'Edit basic properties of any role, except the Site Administrators role (unless you are the root user).', + ]), + 'update_role_field' => new Permission([ + 'slug' => 'update_role_field', + 'name' => 'Edit role', + 'conditions' => "is_master(self.id) || (!has_role(self.id,role.id) && role.id != {$defaultRoleIds['site-admin']} && subset(fields,['slug','name','description']))", + 'description' => 'Edit basic properties of any role, except the Site Administrators role (unless you are the root user).', + ]), + 'delete_role_any' => new Permission([ + 'slug' => 'delete_role', + 'name' => 'Delete role', + 'conditions' => 'always()', + 'description' => 'Delete a role.', + ]), + 'delete_role' => new Permission([ + 'slug' => 'delete_role', + 'name' => 'Delete role', + 'conditions' => "is_master(self.id) || (!has_role(self.id,role.id) && role.id != {$defaultRoleIds['site-admin']})", + 'description' => 'Delete a role, except the Site Administrators role (unless you are the root user).', + ]), + ] + ); + } + + /** + * Save permissions. + * + * @param array $permissions + */ + protected function savePermissions(array &$permissions) + { + foreach ($permissions as $slug => $permission) { + // Trying to find if the permission already exist + $existingPermission = Permission::where(['slug' => $permission->slug, 'conditions' => $permission->conditions])->first(); + + // Don't save if already exist, use existing permission reference + // otherwise to re-sync permissions and roles + if ($existingPermission == null) { + $permission->save(); + } else { + $permissions[$slug] = $existingPermission; + } + } + } + + /** + * Sync permissions with default roles. + * + * @param array $permissions + */ + protected function syncPermissionsRole(array $permissions) + { + parent::syncPermissionsRole($permissions); + + $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); + if ($roleSiteAdmin) { + $roleSiteAdmin->permissions()->syncWithoutDetaching([ + $permissions['uri_dashboard']->id, + $permissions['uri_role']->id, + $permissions['uri_roles']->id, + $permissions['uri_permission']->id, + $permissions['uri_permissions']->id, + // Too much power: $permissions['create_role']->id, + $permissions['view_role_field']->id, + $permissions['update_role_field']->id, + // Too much power: $permissions['update_role_permissions']->id, + // Too much power: $permissions['update_role_permissions_limited']->id, + // Too much power: $permissions['delete_role']->id, + // Too much power: $permissions['delete_role_any']->id, + ]); + } + + $roleGroupAdmin = Role::where('slug', 'group-admin')->first(); + if ($roleGroupAdmin) { + $roleGroupAdmin->permissions()->syncWithoutDetaching([ + $permissions['uri_dashboard']->id, + ]); + } + + $roleUser = Role::where('slug', 'user')->first(); + if ($roleUser) { + $roleUser->permissions()->detach($permissions['uri_dashboard']); + } + } +} diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php new file mode 100644 index 0000000..84c5606 --- /dev/null +++ b/src/ServicesProvider/ServicesProvider.php @@ -0,0 +1,107 @@ +authorizer; + + $currentUser = $c->authenticator->user(); + + if ($authorizer->checkAccess($currentUser, 'uri_dashboard')) { + return $response->withHeader('UF-Redirect', $c->router->pathFor('dashboard')); + } else { + return $response->withHeader('UF-Redirect', $c->router->pathFor('index')); + } + }; + }; + + + /* + * Extend the 'authorizer' service to fix some callbacks + * + * @return \UserFrosting\Sprinkle\Core\Util\ClassMapper + */ + $container->extend('authorizer', function ($authorizer, $c) { + /* + * Check if all values in the array $needle are present in the values of $haystack. + * + * @param array[mixed] $needle the array whose values we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every value in $needle is present in the values of $haystack, false otherwise. + */ + $authorizer->addCallback( + 'subset', + function ($needle, $haystack) { + if (!is_countable($needle)) { + $needle = [ $needle ]; + } + return count($needle) == count(array_intersect($needle, $haystack)); + } + ); + + /* + * Check if all keys of the array $needle are present in the values of $haystack. + * + * This function is useful for whitelisting an array of key-value parameters. + * @param array[mixed] $needle the array whose keys we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every key in $needle is present in the values of $haystack, false otherwise. + */ + $authorizer->addCallback( + 'subset_keys', + function ($needle, $haystack) { + if (!is_countable($needle)) { + $needle = [ $needle ]; + } + return count($needle) == count(array_intersect(array_keys($needle), $haystack)); + } + ); + + return $authorizer; + }); + } +} \ No newline at end of file diff --git a/src/UFTweaks.php b/src/UFTweaks.php new file mode 100644 index 0000000..ebbb524 --- /dev/null +++ b/src/UFTweaks.php @@ -0,0 +1,21 @@ + +
+ {% if checkAccess('uri_users') %} + {% set dashboard_is_empty = false %} +
+ +
+ +
+ {{ translate("USER", 2) }} + {{counter.users}} +
+ +
+ +
+
+ + {% endif %} + + {% if checkAccess('uri_roles') %} + {% set dashboard_is_empty = false %} +
+ +
+ +
+ {{ translate("ROLE", 2) }} + {{counter.roles}} +
+ +
+ +
+
+ + {% endif %} + + {% if checkAccess('uri_groups') %} + {% set dashboard_is_empty = false %} +
+ +
+ +
+ {{ translate("GROUP", 2) }} + {{counter.groups}} +
+ +
+ +
+
+ + {% endif %} +
+ + + +
+ {% if checkAccess('view_system_info') %} + {% set dashboard_is_empty = false %} +
+
+
+

{{translate("SYSTEM_INFO")}}

+
+ +
+
+
{{translate("SYSTEM_INFO.UF_VERSION")}}
+
{{info.version.UF}}
+ +
{{translate("SYSTEM_INFO.PHP_VERSION")}}
+
{{info.version.php}}
+ +
{{translate("SYSTEM_INFO.SERVER")}}
+
{{info.environment.SERVER_SOFTWARE}}
+ +
{{translate("SYSTEM_INFO.DB_VERSION")}}
+
{{info.version.database.type}} {{info.version.database.version}}
+ +
{{translate("SYSTEM_INFO.DB_NAME")}}
+
{{info.database.name}}
+ +
{{translate("SYSTEM_INFO.DIRECTORY")}}
+
{{info.path.project}}
+ +
{{translate("SYSTEM_INFO.URL")}}
+
{{site.uri.public}}
+ +
{{translate("SYSTEM_INFO.SPRINKLES")}}
+
+
    + {% for sprinkle in sprinkles %} +
  • + {{sprinkle}} +
  • + {% endfor %} +
+
+
+
+ + + +
+ +
+ + {% endif %} + + {% if checkAccess('uri_users') %} + {% set dashboard_is_empty = false %} +
+ +
+
+

{{translate("USER.LATEST")}}

+
+ +
+ + +
+ + + +
+ +
+ + {% endif %} + + {% if checkAccess('uri_activities') %} + {% set dashboard_is_empty = false %} +
+
+
+

{{translate('ACTIVITY', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/activities.html.twig" with { + "table" : { + "id" : "table-activities", + "columns" : ["user"] + } + } + %} +
+
+
+ {% endif %} +
+ + + + {% if current_user.group %} + +
+ {% if checkAccess('uri_group', { + 'group': current_user.group + }) %} + {% set dashboard_is_empty = false %} +
+
+ +
+

{{current_user.group.name}}

+
+ +
+ +
+ + +
+
+ +
+ {{ translate("USER", 2) }} + {{current_user.group.users.count}} +
+ +
+ +
+ + {% endif %} +
+ + + +
+ {% if checkAccess('view_group_field', { + 'group': current_user.group, + 'property': 'users' + }) %} + {% set dashboard_is_empty = false %} +
+
+
+

{{translate('GROUP')}} {{translate('USER', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-group-users", + "columns" : [ + (checkAccess('view_user_field', { "property" : 'activities' }) ? "last_activity" : "") + ] + } + } + %} +
+ +
+
+ {% endif %} +
+ + + {% endif %} + + + {% if dashboard_is_empty %} +
+
+
+ +
+

+ {{translate("WELCOME", { + 'first_name': current_user.first_name + })}} +

+
+
+ User Avatar +
+ +
+ +
+ +
+ + {% endif %} +{% endblock %} + +{% block scripts_page %} + + + + + {{ assets.js('js/pages/dashboard') | raw }} + +{% endblock %} diff --git a/templates/pages/group.html.twig b/templates/pages/group.html.twig new file mode 100644 index 0000000..c9dd9aa --- /dev/null +++ b/templates/pages/group.html.twig @@ -0,0 +1,80 @@ +{% extends "@admin/pages/group.html.twig" %} + +{% block body_matter %} +
+
+
+
+

{{translate('GROUP.SUMMARY')}}

+ {% if 'tools' not in tools.hidden %} +
+
+ + +
+
+ {% endif %} +
+
+
+ +
+ +

{{group.name}}

+ + {% if 'description' not in fields.hidden %} +

+ {{group.description}} +

+ {% endif %} + {% if 'users' not in fields.hidden %} +
+ {{ translate('USER', 2)}} +

+ {{group.users.count}} +

+ {% endif %} + {% block group_profile %}{% endblock %} +
+
+
+
+
+
+

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

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-group-users" + } + } + %} +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/pages/role.html.twig b/templates/pages/role.html.twig new file mode 100644 index 0000000..3754781 --- /dev/null +++ b/templates/pages/role.html.twig @@ -0,0 +1,108 @@ +{% extends "@admin/pages/role.html.twig" %} + +{% block body_matter %} +
+
+
+
+

{{translate('ROLE.SUMMARY')}}

+ {% if 'tools' not in tools.hidden %} +
+
+ + +
+
+ {% endif %} +
+
+
+ +
+ +

{{role.name}}

+ + {% if 'description' not in fields.hidden %} +

+ {{role.description}} +

+ {% endif %} + {% if 'users' not in fields.hidden %} +
+ {{ translate('USER', 2)}} +

+ {{role.users.count}} +

+ {% endif %} +
+
+
+
+ {% if (checkAccess('view_role_field', { property: 'permissions' })) %} +
+
+

{{translate('PERMISSION', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/permissions.html.twig" with { + "table" : { + "id" : "table-role-permissions" + } + } + %} +
+
+ {% endif %} +
+
+ {% if (checkAccess('view_role_field', { property: 'users' })) %} +
+
+

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

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-role-users", + "columns" : ["last_activity"] + } + } + %} +
+
+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/pages/roles.html.twig b/templates/pages/roles.html.twig new file mode 100644 index 0000000..a2653be --- /dev/null +++ b/templates/pages/roles.html.twig @@ -0,0 +1,29 @@ +{% extends "@admin/pages/roles.html.twig" %} + +{% block body_matter %} +
+
+
+
+

{{translate('ROLE', 2)}}

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/roles.html.twig" with { + "table" : { + "id" : "table-roles" + } + } + %} +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/pages/users.html.twig b/templates/pages/users.html.twig new file mode 100644 index 0000000..3f3265b --- /dev/null +++ b/templates/pages/users.html.twig @@ -0,0 +1,32 @@ +{% extends "@admin/pages/users.html.twig" %} + +{% block body_matter %} +
+
+
+
+

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

+ {% include "tables/table-tool-menu.html.twig" %} +
+
+ {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-users", + "columns" : [ + (checkAccess('view_user_field', { "property" : 'activities' }) ? "last_activity" : "") + ] + } + } + %} +
+ {% if checkAccess('create_user') %} + + {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/tables/roles.html.twig b/templates/tables/roles.html.twig new file mode 100644 index 0000000..046acd9 --- /dev/null +++ b/templates/tables/roles.html.twig @@ -0,0 +1,80 @@ +{# This partial template renders a table of roles, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + + + + + + + + + + +
{{translate('ROLE')}} {{translate('DESCRIPTION')}} {{translate('ACTIONS')}}
+{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of + + + + + {% endverbatim %} +{% endblock %} diff --git a/templates/tables/users.html.twig b/templates/tables/users.html.twig new file mode 100644 index 0000000..d4d93de --- /dev/null +++ b/templates/tables/users.html.twig @@ -0,0 +1,156 @@ +{# This partial template renders a table of users, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + + + + + {% if 'last_activity' in table.columns %} + + {% endif %} + {% if 'via_roles' in table.columns %} + + {% endif %} + + + + + + +
{{translate('USER')}} {{translate("ACTIVITY.LAST")}} {{translate('PERMISSION.VIA_ROLES')}}{{translate("STATUS")}} {{translate("ACTIONS")}}
+{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of + + + + + + + + {% endverbatim %} +{% endblock %}