diff --git a/components/forms/AddToGroupsForm.js b/components/forms/AddToGroupsForm.js
old mode 100644
new mode 100755
index a9b06d92..543f5132
--- a/components/forms/AddToGroupsForm.js
+++ b/components/forms/AddToGroupsForm.js
@@ -1,5 +1,5 @@
import { useFormikContext } from "formik";
-import { writeUsers } from "lib/Constants";
+import { promoteToSuperAdmin, SUPER_ADMIN_ID, writeUsers } from "lib/Constants";
import AdminRenderer from "@/components/renderers/admin/AdminRenderer";
import { useContext } from "react";
import CacheContext from "../contexts/CacheContext";
@@ -16,21 +16,34 @@ export default function AddToGroupsForm({ groups }) {
Groups
System supplied
+ {capabilities.includes(promoteToSuperAdmin) &&
+
+
Reserved group for KlaudSol installation and setup.
+
}
{systemSupplied.map((group) => (
-
-
-
{group.description}
-
- ))}
+
+
+
{group.description}
+
+ ))}
{userCreated.length > 0 &&
<>
diff --git a/db/migrations/20230518141105_insert_promote_to_super_admin_capability.json b/db/migrations/20230518141105_insert_promote_to_super_admin_capability.json
new file mode 100644
index 00000000..919aa21f
--- /dev/null
+++ b/db/migrations/20230518141105_insert_promote_to_super_admin_capability.json
@@ -0,0 +1,9 @@
+{
+ "up": [
+ "INSERT INTO capabilities (name, description, is_system_supplied) VALUES",
+ "('promote_to_super_admin', \"Can create/promote a user to Super Admin\", true)"
+ ],
+ "down":[
+ "DELETE FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true"
+ ]
+}
diff --git a/db/migrations/20230518141330_connect_promote_super_admin_capability_to_super_admin.json b/db/migrations/20230518141330_connect_promote_super_admin_capability_to_super_admin.json
new file mode 100644
index 00000000..a8c0e0c0
--- /dev/null
+++ b/db/migrations/20230518141330_connect_promote_super_admin_capability_to_super_admin.json
@@ -0,0 +1,10 @@
+{
+ "up": [
+ "INSERT INTO group_capabilities (group_id, capabilities_id) VALUES",
+ "(1, (SELECT id FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true))"
+ ],
+ "down":[
+ "DELETE FROM group_capabilities WHERE group_id IN = 1",
+ "AND capabilities_id = (SELECT id FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true);"
+ ]
+}
diff --git a/db/migrations/20230522085940_disconnect_promote_super_admin_capability_to_super_admin.json b/db/migrations/20230522085940_disconnect_promote_super_admin_capability_to_super_admin.json
new file mode 100644
index 00000000..0dcdbb5f
--- /dev/null
+++ b/db/migrations/20230522085940_disconnect_promote_super_admin_capability_to_super_admin.json
@@ -0,0 +1,10 @@
+{
+ "up": [
+ "DELETE FROM group_capabilities WHERE group_id = 1 ",
+ "AND capabilities_id = (SELECT id FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true);"
+ ],
+ "down": [
+ "INSERT INTO group_capabilities (group_id, capabilities_id) VALUES",
+ "(1, (SELECT id FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true))"
+ ]
+}
diff --git a/db/migrations/20230522090030_delete_promote_to_super_admin_capability.json b/db/migrations/20230522090030_delete_promote_to_super_admin_capability.json
new file mode 100644
index 00000000..9f660ae9
--- /dev/null
+++ b/db/migrations/20230522090030_delete_promote_to_super_admin_capability.json
@@ -0,0 +1,9 @@
+{
+ "up": [
+ "DELETE FROM capabilities WHERE name = 'promote_to_super_admin' AND is_system_supplied = true"
+ ],
+ "down": [
+ "INSERT INTO capabilities (name, description, is_system_supplied) VALUES",
+ "('promote_to_super_admin', \"Can create/promote a user to Super Admin\", true)"
+ ]
+}
diff --git a/db/migrations/20230522090410_insert_assign_to_group_capability.json b/db/migrations/20230522090410_insert_assign_to_group_capability.json
new file mode 100644
index 00000000..9e52604e
--- /dev/null
+++ b/db/migrations/20230522090410_insert_assign_to_group_capability.json
@@ -0,0 +1,9 @@
+{
+ "up": [
+ "INSERT INTO capabilities (name, description, is_system_supplied) VALUES",
+ "('assign_to_group', \"Can assign a user to a group\", true)"
+ ],
+ "down":[
+ "DELETE FROM capabilities WHERE name = 'assign_to_group' AND is_system_supplied = true"
+ ]
+}
diff --git a/db/migrations/20230522090520_connect_assign_to_group_capability_to_super_admin.json b/db/migrations/20230522090520_connect_assign_to_group_capability_to_super_admin.json
new file mode 100644
index 00000000..38fa5734
--- /dev/null
+++ b/db/migrations/20230522090520_connect_assign_to_group_capability_to_super_admin.json
@@ -0,0 +1,10 @@
+{
+ "up": [
+ "INSERT INTO group_capabilities (group_id, capabilities_id, params1) VALUES ",
+ "(1, (SELECT id FROM capabilities WHERE name = 'assign_to_group' AND is_system_supplied = true), 1)"
+ ],
+ "down":[
+ "DELETE FROM group_capabilities WHERE group_id IN = 1 ",
+ "AND capabilities_id = (SELECT id FROM capabilities WHERE name = 'assign_to_group' AND is_system_supplied = true);"
+ ]
+}
diff --git a/db/migrations/plugins/README.md b/db/migrations/plugins/README.md
index aad92e49..84b37bef 100644
--- a/db/migrations/plugins/README.md
+++ b/db/migrations/plugins/README.md
@@ -1,3 +1,3 @@
This directory stores all KlaudSol CMS plugins.
-This directory is being ignored by Git.
\ No newline at end of file
+This directory is being ignored by Git.
diff --git a/lib/Constants.js b/lib/Constants.js
index b6392343..70550689 100644
--- a/lib/Constants.js
+++ b/lib/Constants.js
@@ -6,6 +6,9 @@ export const maximumNumberOfPage = 10;
export const EntryValues = [10, 20, 30, 40, 50];
export const validImageTypes = "image/png, image/gif, image/jpeg, image/jfif,image/svg+xml";
+// group id's
+export const SUPER_ADMIN_ID = 1;
+
// capabilities
export const readContents = 'read_contents';
export const writeContents = 'write_contents';
@@ -26,9 +29,11 @@ export const editProfile = 'edit_profile';
export const approveUsers = 'approve_users';
export const rejectUsers = 'reject_users';
export const changeUserPassword = 'change_user_password';
-export const deleteUsers = 'delete_users'
-
+export const deleteUsers = 'delete_users';
+export const assignToGroup = 'assign_to_group';
+export const promoteToSuperAdmin = `${assignToGroup}(${SUPER_ADMIN_ID})`;
// password creation
export const AUTO_PASSWORD = 'autogenerated';
export const CUSTOM_PASSWORD = 'custom'
+
diff --git a/pages/admin/users/[id]/index.js b/pages/admin/users/[id]/index.js
index ac7c1179..4f1ca407 100644
--- a/pages/admin/users/[id]/index.js
+++ b/pages/admin/users/[id]/index.js
@@ -7,6 +7,8 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef } from "react";
import { slsFetch } from "@klaudsol/commons/lib/Client";
+import Groups from '@klaudsol/commons/models/Groups';
+import People from '@klaudsol/commons/models/People';
import AppBackButton from "@/components/klaudsolcms/buttons/AppBackButton";
import AppButtonLg from "@/components/klaudsolcms/buttons/AppButtonLg";
@@ -16,7 +18,7 @@ import AppInfoModal from "@/components/klaudsolcms/modals/AppInfoModal";
import { FaCheck, FaTrash, FaArrowRight } from "react-icons/fa";
import { Formik, Form } from "formik";
import ContentManagerLayout from "components/layouts/ContentManagerLayout";
-import { changeUserPassword, DEFAULT_SKELETON_ROW_COUNT, deleteUsers, writeUsers } from "lib/Constants";
+import { changeUserPassword, DEFAULT_SKELETON_ROW_COUNT, deleteUsers, SUPER_ADMIN_ID, writeUsers } from "lib/Constants";
import {
LOADING,
@@ -31,7 +33,7 @@ import useUserReducer from "@/components/reducers/userReducer";
import UserForm from "@/components/forms/UserForm";
import AddToGroupsForm from "@/components/forms/AddToGroupsForm";
-export default function UserInfo({ cache }) {
+export default function UserInfo({ cache, groups, user }) {
const [state, setState] = useUserReducer();
const router = useRouter();
@@ -44,44 +46,6 @@ export default function UserInfo({ cache }) {
const { entity_type_slug, id } = router.query;
const formRef = useRef();
- useEffect(() => {
- (async () => {
- try {
- setState(LOADING, true);
-
- const userDataUrl = `/api/admin/users/${id}`;
- const userParams = {
- headers: {
- 'Content-Type': 'application/json',
- }
- }
-
- const groupsUrl = `/api/admin/groups`;
- const groupsParams = {
- headers: {
- 'Content-Type': 'application/json',
- }
- }
-
- const groupsPromise = slsFetch(groupsUrl, groupsParams);
- const userPromise = slsFetch(userDataUrl, userParams);
- const [groupsRaw, userRaw] = await Promise.all([groupsPromise, userPromise]);
-
- const { data: user } = await userRaw.json();
- const { person, groups: userGroups } = user;
-
- const { data: groups } = await groupsRaw.json();
-
- setState(SET_VALUES, { ...person[0], groups: userGroups });
- setState(SET_GROUPS, groups);
- } catch (ex) {
- errorHandler(ex);
- } finally {
- setState(LOADING, false);
- }
- })();
- }, []);
-
const onSubmit = (e) => {
e.preventDefault();
formRef.current.handleSubmit();
@@ -112,16 +76,16 @@ export default function UserInfo({ cache }) {
const formikParams = {
innerRef: formRef,
- initialValues: state.user,
+ initialValues: user,
onSubmit: (values) => {
(async () => {
try {
setState(SAVING, true);
- const isSameEmail = values.email === state.user.email;
+ const isSameEmail = values.email === user.email;
- const toAdd = values.groups.filter((group) => !state.user.groups.includes(group));
- const toDelete = state.user.groups.filter((group) => !values.groups.includes(group));
+ const toAdd = values.groups.filter((group) => !user.groups.includes(group));
+ const toDelete = user.groups.filter((group) => !values.groups.includes(group));
const url = `/api/admin/users/${id}`
const params = {
@@ -190,7 +154,7 @@ export default function UserInfo({ cache }) {
@@ -234,4 +198,19 @@ export default function UserInfo({ cache }) {
);
}
-export const getServerSideProps = getSessionCache();
+export const getServerSideProps = getSessionCache(async ({ query }) => {
+ const { id } = query;
+
+ const [groups, userGroups, user] = await Promise.all([
+ Groups.all(),
+ Groups.findByUser({ id }),
+ People.get({ id })
+ ])
+
+ return {
+ props: {
+ groups: await groups.filter((group) => group.id !== SUPER_ADMIN_ID),
+ user: { ...user[0], groups: userGroups },
+ }
+ }
+});
diff --git a/pages/admin/users/create.js b/pages/admin/users/create.js
index 7fad3b65..a882d57b 100644
--- a/pages/admin/users/create.js
+++ b/pages/admin/users/create.js
@@ -7,6 +7,7 @@ import { useRouter } from "next/router";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { slsFetch } from "@klaudsol/commons/lib/Client";
+import Groups from '@klaudsol/commons/models/Groups';
import AppBackButton from "@/components/klaudsolcms/buttons/AppBackButton";
import AppButtonLg from "@/components/klaudsolcms/buttons/AppButtonLg";
@@ -16,7 +17,7 @@ import AppInfoModal from "@/components/klaudsolcms/modals/AppInfoModal";
import { FaCheck } from "react-icons/fa";
import { Formik, Form } from "formik";
import ContentManagerLayout from "components/layouts/ContentManagerLayout";
-import { DEFAULT_SKELETON_ROW_COUNT, writeGroups, writeUsers } from "@/lib/Constants";
+import { DEFAULT_SKELETON_ROW_COUNT, SUPER_ADMIN_ID, writeGroups, writeUsers } from "@/lib/Constants";
import {
LOADING,
@@ -34,7 +35,7 @@ import PasswordForm from "@/components/forms/PasswordForm";
const USER_INFO = 'user_info';
const ADD_GROUPS = 'add_groups';
-export default function Type({ cache }) {
+export default function Type({ cache, groups }) {
const [state, setState] = useUserReducer();
const [currentPage, setCurrentPage] = useState(USER_INFO);
@@ -53,30 +54,6 @@ export default function Type({ cache }) {
formRef.current.handleSubmit();
};
- useEffect(() => {
- (async () => {
- try {
- setState(LOADING, true);
-
- const url = `/api/admin/groups`;
- const params = {
- headers: {
- 'Content-Type': 'application/json',
- }
- }
-
- const resRaw = await slsFetch(url, params);
- const { data } = await resRaw.json();
-
- setState(SET_GROUPS, data);
- } catch (err) {
- errorHandler(err);
- } finally {
- setState(LOADING, false);
- }
- })()
- }, []);
-
const formikParams = {
innerRef: formRef,
initialValues: {
@@ -177,7 +154,7 @@ export default function Type({ cache }) {
>
}
- {currentPage === ADD_GROUPS && }
+ {currentPage === ADD_GROUPS && }
@@ -218,4 +195,10 @@ export default function Type({ cache }) {
);
}
-export const getServerSideProps = getSessionCache();
+export const getServerSideProps = getSessionCache(async (context) => {
+ const groupsRaw = await Groups.all();
+ const groups = await groupsRaw.filter((group) => group.id !== SUPER_ADMIN_ID);
+
+ return { props: { groups }}
+});
+
diff --git a/pages/api/admin/groups/index.js b/pages/api/admin/groups/index.js
deleted file mode 100644
index 46f57ae4..00000000
--- a/pages/api/admin/groups/index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { handleRequests } from "@klaudsol/commons/lib/API";
-import { withSession } from "@klaudsol/commons/lib/Session";
-import { createHash } from '@/lib/Hash';
-import { assertUserCan } from '@klaudsol/commons/lib/Permissions';
-import { readGroups } from "@/lib/Constants";
-import { OK, NOT_FOUND } from '@klaudsol/commons/lib/HttpStatuses';
-import Groups from '@klaudsol/commons/models/Groups';
-
-export default withSession(handleRequests({ get }));
-
-async function get(req, res) {
- await assertUserCan(readGroups, req);
-
- const groups = await Groups.all();
-
- const output = {
- data: groups,
- metadata: {},
- };
-
- output.metadata.hash = createHash(output);
-
- groups ? res.status(OK).json(output ?? []) : res.status(NOT_FOUND).json({});
-}
diff --git a/pages/api/admin/users/[id]/index.js b/pages/api/admin/users/[id]/index.js
index 7cc4acc0..fa835def 100644
--- a/pages/api/admin/users/[id]/index.js
+++ b/pages/api/admin/users/[id]/index.js
@@ -1,7 +1,7 @@
import { handleRequests } from "@klaudsol/commons/lib/API";
import { withSession } from "@klaudsol/commons/lib/Session";
import { createHash } from '@/lib/Hash';
-import { deleteUsers, readUsers, writeUsers } from "@/lib/Constants";
+import { deleteUsers, assignToGroup, readUsers, SUPER_ADMIN_ID, writeGroups, writeUsers } from "@/lib/Constants";
import { assertUserCan } from "@klaudsol/commons/lib/Permissions";
import { OK, NOT_FOUND } from '@klaudsol/commons/lib/HttpStatuses';
import InsufficientDataError from '@klaudsol/commons/errors/InsufficientDataError';
@@ -30,11 +30,15 @@ async function get(req, res) {
}
async function put(req, res) {
- await assertUserCan(writeUsers, req);
-
- const { id } = req.query;
+ const { id } = req.query;
const { firstName, lastName, forcePasswordChange, loginEnabled, approved, email, isSameEmail, toAdd, toDelete } = req.body;
+ await assertUserCan(writeUsers, req) &&
+ // If the user wants to edit the groups of another user
+ ((toAdd.length > 0 || toDelete.length > 0) && await assertUserCan(writeGroups, req)) &&
+ // If the user wants to turn another user into a superadmin
+ (toAdd.includes(SUPER_ADMIN_ID.toString()) || toDelete.includes(SUPER_ADMIN_ID.toString())) && await assertUserCan(assignToGroup, req, SUPER_ADMIN_ID)
+
if (!firstName) throw new InsufficientDataError('Please enter your first name.');
if (!lastName) throw new InsufficientDataError('Please enter your last name.');
if (!email) throw new InsufficientDataError('Please enter your email.');
diff --git a/pages/api/admin/users/index.js b/pages/api/admin/users/index.js
index 7a367c23..9fc7180b 100644
--- a/pages/api/admin/users/index.js
+++ b/pages/api/admin/users/index.js
@@ -7,7 +7,7 @@ import InsufficientDataError from '@klaudsol/commons/errors/InsufficientDataErro
import UserAlreadyExists from "@klaudsol/commons/errors/UserAlreadyExists";
import People from '@klaudsol/commons/models/People';
import PeopleGroups from '@klaudsol/commons/models/PeopleGroups';
-import { readPendingUsers, readUsers, writeGroups, writeUsers } from "@/lib/Constants";
+import { assignToGroup, readPendingUsers, readUsers, SUPER_ADMIN_ID, writeGroups, writeUsers } from "@/lib/Constants";
export default withSession(handleRequests({ get, post }));
@@ -30,21 +30,23 @@ async function get(req, res) {
}
async function post(req, res) {
-
- const {
- firstName,
- lastName,
- email,
- password,
- confirmPassword,
- groups = [],
+ const {
+ firstName,
+ lastName,
+ email,
+ password,
+ confirmPassword,
+ groups = [],
approved = false,
- loginEnabled = false,
- forcePasswordChange = false
+ loginEnabled = false,
+ forcePasswordChange = false
} = req.body;
- await assertUserCan(writeUsers, req);
- if (groups.length > 0) await assertUserCan(writeGroups, req);
+ await assertUserCan(writeUsers, req) &&
+ // If the user wants to add groups when creating a user
+ (groups.length > 0 && await assertUserCan(writeGroups, req)) &&
+ // If the user wants to create a super admin user
+ (groups.includes(SUPER_ADMIN_ID.toString()) && await assertUserCan(assignToGroup, req, SUPER_ADMIN_ID))
if (!firstName) throw new InsufficientDataError('Please enter your first name.');
if (!lastName) throw new InsufficientDataError('Please enter your last name.');