diff --git a/modules/backend/controllers/UserGroups.php b/modules/backend/controllers/UserGroups.php
index 3db4d78ee..97b55e399 100644
--- a/modules/backend/controllers/UserGroups.php
+++ b/modules/backend/controllers/UserGroups.php
@@ -31,30 +31,4 @@ class UserGroups extends Controller
BackendMenu::setContext('October.System', 'system', 'users');
SettingsManager::setContext('October.System', 'administrators');
}
-
- /**
- * Add available permission fields to the Group form.
- */
- public function formExtendFields($form)
- {
- /*
- * Add permissions tab
- */
- $form->addTabFields($this->generatePermissionsField());
- }
-
- /**
- * Adds the permissions editor widget to the form.
- * @return array
- */
- protected function generatePermissionsField()
- {
- return [
- 'permissions' => [
- 'tab' => 'backend::lang.user.permissions',
- 'type' => 'Backend\FormWidgets\PermissionEditor',
- 'mode' => 'checkbox'
- ]
- ];
- }
}
diff --git a/modules/backend/controllers/UserRoles.php b/modules/backend/controllers/UserRoles.php
new file mode 100644
index 000000000..e6b9600f9
--- /dev/null
+++ b/modules/backend/controllers/UserRoles.php
@@ -0,0 +1,60 @@
+addTabFields($this->generatePermissionsField());
+ }
+
+ /**
+ * Adds the permissions editor widget to the form.
+ * @return array
+ */
+ protected function generatePermissionsField()
+ {
+ return [
+ 'permissions' => [
+ 'tab' => 'backend::lang.user.permissions',
+ 'type' => 'Backend\FormWidgets\PermissionEditor',
+ 'mode' => 'checkbox'
+ ]
+ ];
+ }
+}
diff --git a/modules/backend/controllers/usergroups/create.htm b/modules/backend/controllers/usergroups/create.htm
index fcc511965..7e8cf3665 100644
--- a/modules/backend/controllers/usergroups/create.htm
+++ b/modules/backend/controllers/usergroups/create.htm
@@ -41,4 +41,4 @@
= e(trans($this->fatalError)) ?>
= e(trans('backend::lang.user.group.return')) ?>
-
\ No newline at end of file
+
diff --git a/modules/backend/controllers/usergroups/update.htm b/modules/backend/controllers/usergroups/update.htm
index 3b19df71b..a262441d7 100644
--- a/modules/backend/controllers/usergroups/update.htm
+++ b/modules/backend/controllers/usergroups/update.htm
@@ -50,4 +50,4 @@
= e(trans($this->fatalError)) ?>
= e(trans('backend::lang.user.group.return')) ?>
-
\ No newline at end of file
+
diff --git a/modules/backend/controllers/userroles/_list_toolbar.htm b/modules/backend/controllers/userroles/_list_toolbar.htm
new file mode 100644
index 000000000..29e576249
--- /dev/null
+++ b/modules/backend/controllers/userroles/_list_toolbar.htm
@@ -0,0 +1,8 @@
+
diff --git a/modules/backend/controllers/userroles/config_form.yaml b/modules/backend/controllers/userroles/config_form.yaml
new file mode 100644
index 000000000..68b4063b7
--- /dev/null
+++ b/modules/backend/controllers/userroles/config_form.yaml
@@ -0,0 +1,16 @@
+# ===================================
+# Form Behavior Config
+# ===================================
+
+name: backend::lang.user.role.name
+form: ~/modules/backend/models/userrole/fields.yaml
+modelClass: Backend\Models\UserRole
+defaultRedirect: backend/userroles
+
+create:
+ redirect: backend/userroles/update/:id
+ redirectClose: backend/userroles
+
+update:
+ redirect: backend/userroles
+ redirectClose: backend/userroles
diff --git a/modules/backend/controllers/userroles/config_list.yaml b/modules/backend/controllers/userroles/config_list.yaml
new file mode 100644
index 000000000..5871e58fd
--- /dev/null
+++ b/modules/backend/controllers/userroles/config_list.yaml
@@ -0,0 +1,15 @@
+# ===================================
+# List Behavior Config
+# ===================================
+
+title: backend::lang.user.role.list_title
+list: ~/modules/backend/models/userrole/columns.yaml
+modelClass: Backend\Models\UserRole
+recordUrl: backend/userroles/update/:id
+noRecordsMessage: backend::lang.list.no_records
+recordsPerPage: 5
+
+toolbar:
+ buttons: list_toolbar
+ search:
+ prompt: backend::lang.list.search_prompt
diff --git a/modules/backend/controllers/userroles/create.htm b/modules/backend/controllers/userroles/create.htm
new file mode 100644
index 000000000..07f55b3b1
--- /dev/null
+++ b/modules/backend/controllers/userroles/create.htm
@@ -0,0 +1,44 @@
+
+
+
+
+fatalError): ?>
+
+ = Form::open(['class'=>'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.user.role.return')) ?>
+
diff --git a/modules/backend/controllers/userroles/index.htm b/modules/backend/controllers/userroles/index.htm
new file mode 100644
index 000000000..ee75b8527
--- /dev/null
+++ b/modules/backend/controllers/userroles/index.htm
@@ -0,0 +1,8 @@
+
+
+
+
+= $this->listRender() ?>
diff --git a/modules/backend/controllers/userroles/update.htm b/modules/backend/controllers/userroles/update.htm
new file mode 100644
index 000000000..7f1b97736
--- /dev/null
+++ b/modules/backend/controllers/userroles/update.htm
@@ -0,0 +1,53 @@
+
+
+
+
+fatalError): ?>
+
+ = Form::open(['class'=>'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.user.role.return')) ?>
+
diff --git a/modules/backend/controllers/users/_list_toolbar.htm b/modules/backend/controllers/users/_list_toolbar.htm
index 69ff34f5e..096c4d68c 100644
--- a/modules/backend/controllers/users/_list_toolbar.htm
+++ b/modules/backend/controllers/users/_list_toolbar.htm
@@ -2,6 +2,9 @@
= e(trans('backend::lang.user.new')) ?>
+
+ = e(trans('backend::lang.user.role.list_title')) ?>
+
= e(trans('backend::lang.user.group.list_title')) ?>
diff --git a/modules/backend/database/migrations/2013_10_01_000001_Db_Backend_Users.php b/modules/backend/database/migrations/2013_10_01_000001_Db_Backend_Users.php
index 15069a180..caa90669e 100644
--- a/modules/backend/database/migrations/2013_10_01_000001_Db_Backend_Users.php
+++ b/modules/backend/database/migrations/2013_10_01_000001_Db_Backend_Users.php
@@ -20,6 +20,7 @@ class DbBackendUsers extends Migration
$table->string('reset_password_code')->nullable()->index('reset_code_index');
$table->text('permissions')->nullable();
$table->boolean('is_activated')->default(0);
+ $table->integer('role_id')->unsigned()->nullable()->index('admin_role_index');
$table->timestamp('activated_at')->nullable();
$table->timestamp('last_login')->nullable();
$table->timestamps();
diff --git a/modules/backend/database/migrations/2013_10_01_000002_Db_Backend_User_Groups.php b/modules/backend/database/migrations/2013_10_01_000002_Db_Backend_User_Groups.php
index f656ab296..599707c46 100644
--- a/modules/backend/database/migrations/2013_10_01_000002_Db_Backend_User_Groups.php
+++ b/modules/backend/database/migrations/2013_10_01_000002_Db_Backend_User_Groups.php
@@ -11,7 +11,6 @@ class DbBackendUserGroups extends Migration
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->unique('name_unique');
- $table->text('permissions')->nullable();
$table->timestamps();
});
}
diff --git a/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php b/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php
new file mode 100644
index 000000000..4c49c3dd7
--- /dev/null
+++ b/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php
@@ -0,0 +1,143 @@
+engine = 'InnoDB';
+ $table->increments('id');
+ $table->string('name')->unique('role_unique');
+ $table->string('code')->nullable()->index('role_code_index');
+ $table->text('description')->nullable();
+ $table->text('permissions')->nullable();
+ $table->timestamps();
+ });
+
+ // This detects older builds and performs a migration to include
+ // the new role system. This column will exist for new installs
+ // so this heavy migration process does not need to execute.
+ $this->migratePreviousBuild();
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('backend_user_roles');
+ }
+
+ protected function migratePreviousBuild()
+ {
+ // Role not found in the users table, perform a complete migration.
+ // Merging group permissions with the user and assigning the user
+ // with the first available role.
+ if (Schema::hasColumn('backend_users', 'role_id')) {
+ Schema::table('backend_users', function (Blueprint $table) {
+ $table->integer('role_id')->unsigned()->nullable()->index('admin_role_index');
+ });
+
+ $this->migratePermissionsFromGroupsToRoles();
+ }
+
+ // Drop permissions column on groups table as it is no longer needed.
+ if (Schema::hasColumn('backend_user_groups', 'permissions')) {
+ Schema::table('backend_user_groups', function (Blueprint $table) {
+ $table->dropColumn('permissions');
+ });
+ }
+ }
+
+ protected function migratePermissionsFromGroupsToRoles()
+ {
+ $groups = Db::table('backend_user_groups')->get();
+ $roles = [];
+ $permissions = [];
+
+ /*
+ * Carbon copy groups to roles
+ */
+ foreach ($groups as $group) {
+ if (!isset($group->name) || !$group->name) {
+ continue;
+ }
+
+ try {
+ $roles[$group->id] = Db::table('backend_user_roles')->insertGetId([
+ 'name' => $group->name,
+ 'permissions' => $group->permissions ?? null
+ ]);
+ }
+ catch (Exception $ex) {}
+
+ $permissions[$group->id] = $group->permissions ?? null;
+ }
+
+ /*
+ * Assign a user with the first available role
+ */
+ $found = [];
+ $joins = Db::table('backend_users_groups')->get();
+
+ foreach ($joins as $join) {
+ if (!$roleId = array_get($roles, $join->user_group_id)) {
+ continue;
+ }
+
+ $userId = $join->user_id;
+
+ if (!isset($found[$userId])) {
+ Db::table('backend_users')->where('id', $userId)->update([
+ 'role_id' => $roleId
+ ]);
+ }
+
+ $found[$userId][] = $join->user_group_id;
+ }
+
+ /*
+ * Merge group permissions in to user
+ */
+ foreach ($found as $userId => $groups) {
+ $userPerms = [];
+
+ foreach ($groups as $groupId) {
+ if (!$permString = array_get($permissions, $groupId)) {
+ continue;
+ }
+
+ try {
+ $perms = json_decode($permString, true);
+ $userPerms = array_merge($userPerms, $perms);
+ }
+ catch (Exception $ex) {}
+ }
+
+ if (count($userPerms) > 0) {
+ $this->splicePermissionsForUser($userId, $userPerms);
+ }
+ }
+ }
+
+ protected function splicePermissionsForUser($userId, $permissions)
+ {
+ /*
+ * Look up user and splice the provided permissions in
+ */
+ $user = Db::table('backend_users')->where('id', $userId)->first();
+ if (!$user) {
+ return;
+ }
+
+ try {
+ $currentPerms = $user->permissions ? json_decode($user->permissions, true) : [];
+ $newPerms = array_merge($permissions, $currentPerms);
+
+ Db::table('backend_users')->where('id', $userId)->update([
+ 'permissions' => json_encode($newPerms)
+ ]);
+ }
+ catch (Exception $ex) {}
+ }
+}
diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php
index ff8036a8d..344e0f5a9 100644
--- a/modules/backend/lang/en/lang.php
+++ b/modules/backend/lang/en/lang.php
@@ -116,8 +116,10 @@ return [
'last_name' => 'Last Name',
'full_name' => 'Full Name',
'email' => 'Email',
+ 'role_field' => 'Role',
+ 'role_comment' => 'Roles define user permissions, which can be overriden on the user level, on the Permissions tab.',
'groups' => 'Groups',
- 'groups_comment' => 'Specify which groups the account should belong to. Groups define user permissions, which can be overriden on the user level, on the Permissions tab.',
+ 'groups_comment' => 'Specify which groups this account should belong to.',
'avatar' => 'Avatar',
'password' => 'Password',
'password_confirmation' => 'Confirm Password',
@@ -138,8 +140,8 @@ return [
'updated_at' => 'Updated at',
'group' => [
'name' => 'Group',
- 'name_comment' => 'The name is displayed in the group list on the Create/Edit Administrator form.',
'name_field' => 'Name',
+ 'name_comment' => 'The name is displayed in the group list on the Create/Edit Administrator form.',
'description_field' => 'Description',
'is_new_user_default_field_label' => 'Default group',
'is_new_user_default_field_comment' => 'Add new administrators to this group by default',
@@ -152,6 +154,20 @@ return [
'return' => 'Return to group list',
'users_count' => 'Users'
],
+ 'role' => [
+ 'name' => 'Role',
+ 'name_field' => 'Name',
+ 'name_comment' => 'The name is displayed in the role list on the Create/Edit Administrator form.',
+ 'description_field' => 'Description',
+ 'code_field' => 'Code',
+ 'code_comment' => 'Enter a unique code if you want to access the role object with the API.',
+ 'menu_label' => 'Manage Roles',
+ 'list_title' => 'Manage Roles',
+ 'new' => 'New Role',
+ 'delete_confirm' => 'Delete this administrator role?',
+ 'return' => 'Return to role list',
+ 'users_count' => 'Users'
+ ],
'preferences' => [
'not_authenticated' => 'There is no an authenticated user to load or save preferences for.'
]
diff --git a/modules/backend/models/User.php b/modules/backend/models/User.php
index d381fbc95..35dab3fc1 100644
--- a/modules/backend/models/User.php
+++ b/modules/backend/models/User.php
@@ -35,6 +35,10 @@ class User extends UserBase
'groups' => [UserGroup::class, 'table' => 'backend_users_groups']
];
+ public $belongsto = [
+ 'role' => UserRole::class
+ ];
+
public $attachOne = [
'avatar' => \System\Models\File::class
];
@@ -144,9 +148,22 @@ class User extends UserBase
public function getGroupsOptions()
{
$result = [];
+
foreach (UserGroup::all() as $group) {
$result[$group->id] = [$group->name, $group->description];
}
+
+ return $result;
+ }
+
+ public function getRoleOptions()
+ {
+ $result = [];
+
+ foreach (UserRole::all() as $role) {
+ $result[$role->id] = [$role->name, $role->description];
+ }
+
return $result;
}
}
diff --git a/modules/backend/models/UserRole.php b/modules/backend/models/UserRole.php
new file mode 100644
index 000000000..d115ebf6e
--- /dev/null
+++ b/modules/backend/models/UserRole.php
@@ -0,0 +1,37 @@
+ 'required|between:2,128|unique:backend_user_roles',
+ ];
+
+ /**
+ * @var array Relations
+ */
+ public $hasMany = [
+ 'users' => [User::class, 'key' => 'role_id'],
+ 'users_count' => [User::class, 'key' => 'role_id', 'count' => true]
+ ];
+}
diff --git a/modules/backend/models/user/fields.yaml b/modules/backend/models/user/fields.yaml
index 5df53a671..caf687e85 100644
--- a/modules/backend/models/user/fields.yaml
+++ b/modules/backend/models/user/fields.yaml
@@ -46,11 +46,17 @@ tabs:
span: right
label: backend::lang.user.password_confirmation
- groups:
+ role:
context: [create, update]
+ label: backend::lang.user.role_field
+ commentAbove: backend::lang.user.role_comment
+ type: radio
+
+ groups:
label: backend::lang.user.groups
commentAbove: backend::lang.user.groups_comment
type: checkboxlist
+ tab: backend::lang.user.groups
secondaryTabs:
fields:
diff --git a/modules/backend/models/usergroup/fields.yaml b/modules/backend/models/usergroup/fields.yaml
index be3d0a483..9a4ab8625 100644
--- a/modules/backend/models/usergroup/fields.yaml
+++ b/modules/backend/models/usergroup/fields.yaml
@@ -22,6 +22,3 @@ fields:
label: backend::lang.user.group.description_field
type: textarea
size: tiny
-
-tabs:
- stretch: true
diff --git a/modules/backend/models/userrole/columns.yaml b/modules/backend/models/userrole/columns.yaml
new file mode 100644
index 000000000..392ddf6bd
--- /dev/null
+++ b/modules/backend/models/userrole/columns.yaml
@@ -0,0 +1,19 @@
+# ===================================
+# Column Definitions
+# ===================================
+
+columns:
+ name:
+ label: backend::lang.user.role.name_field
+ searchable: yes
+
+ description:
+ label: backend::lang.user.role.description_field
+ searchable: yes
+
+ users_count:
+ label: backend::lang.user.role.users_count
+ relation: users_count
+ valueFrom: count
+ default: 0
+ sortable: false
diff --git a/modules/backend/models/userrole/fields.yaml b/modules/backend/models/userrole/fields.yaml
new file mode 100644
index 000000000..f0d7ccfd7
--- /dev/null
+++ b/modules/backend/models/userrole/fields.yaml
@@ -0,0 +1,22 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ name:
+ label: backend::lang.user.role.name_field
+ commentAbove: backend::lang.user.role.name_comment
+ span: auto
+
+ code:
+ label: backend::lang.user.role.code_field
+ commentAbove: backend::lang.user.role.code_comment
+ span: auto
+
+ description:
+ label: backend::lang.user.role.description_field
+ type: textarea
+ size: tiny
+
+tabs:
+ stretch: true