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.');