diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx
index 214fe9891..03713544f 100644
--- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-members.tsx
@@ -144,7 +144,7 @@ const RoleMembers: FC<{ role: Role; isLoadingRole?: boolean }> = ({ role, isLoad
)}
searchBarRightNode={
}
/>
diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx
index 2634c43b9..a2cddaf2d 100644
--- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/role-page.tsx
@@ -24,17 +24,17 @@ function RolePage({ params: { roleId } }: { params: { roleId: string } }) {
const items: Items = role
? [
- {
- key: 'members',
- label: 'Manage Members',
- children: ,
- },
- { key: 'permissions', label: 'Permissions', children: },
{
key: 'generalData',
label: 'General Data',
children: ,
},
+ { key: 'permissions', label: 'Permissions', children: },
+ {
+ key: 'members',
+ label: 'Manage Members',
+ children: ,
+ },
]
: [];
@@ -58,9 +58,11 @@ function RolePage({ params: { roleId } }: { params: { roleId: string } }) {
}
>
-
-
-
+
+
+
+
+
);
}
diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx
index f305babb5..fbd2cc131 100644
--- a/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/roles/[roleId]/roleGeneralData.tsx
@@ -3,7 +3,7 @@
import { toCaslResource } from '@/lib/ability/caslAbility';
import { useGetAsset, usePutAsset } from '@/lib/fetch-data';
import { Alert, App, Button, DatePicker, Form, Input, Spin } from 'antd';
-import { FC } from 'react';
+import { FC, useEffect, useState } from 'react';
import dayjs from 'dayjs';
import germanLocale from 'antd/es/date-picker/locale/de_DE';
import { useAbilityStore } from '@/lib/abilityStore';
@@ -21,6 +21,20 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
onError: () => message.open({ type: 'error', content: 'Something went wrong' }),
});
+ const [submittable, setSubmittable] = useState(false);
+ const values = Form.useWatch('name', form);
+
+ useEffect(() => {
+ form.validateFields({ validateOnly: true }).then(
+ () => {
+ setSubmittable(true);
+ },
+ () => {
+ setSubmittable(false);
+ },
+ );
+ }, [form, values]);
+
if (isLoading || error || !data) return ;
const role = toCaslResource('Role', data);
@@ -51,7 +65,13 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
>
)}
-
+
+
@@ -73,8 +93,8 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
-
diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx
index f616646f0..81f3d547e 100644
--- a/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx
@@ -1,68 +1,34 @@
'use client';
-import { AuthCan } from '@/lib/clientAuthComponents';
-import { ApiRequestBody, usePostAsset } from '@/lib/fetch-data';
+import { usePostAsset } from '@/lib/fetch-data';
import { PlusOutlined } from '@ant-design/icons';
-import { Button, Form, App, Input, Modal } from 'antd';
+import { Button, Form, App, Input, Modal, DatePicker } from 'antd';
import { FC, ReactNode, useEffect, useState } from 'react';
+import dayjs from 'dayjs';
+import germanLocale from 'antd/es/date-picker/locale/de_DE';
+import { useRouter } from 'next/navigation';
+import { AuthCan } from '@/lib/clientAuthComponents';
+
+type PostRoleKeys = 'name' | 'description' | 'expiration';
-type PostUserField = keyof ApiRequestBody<'/users', 'post'>;
-
-const modalStructureWithoutPassword: {
- dataKey: PostUserField;
- label: string;
- type: string;
-}[] = [
- {
- dataKey: 'firstName',
- label: 'First Name',
- type: 'text',
- },
- {
- dataKey: 'lastName',
- label: 'Last Name',
- type: 'text',
- },
- {
- dataKey: 'username',
- label: 'Username Name',
- type: 'text',
- },
- {
- dataKey: 'email',
- label: 'Email',
- type: 'email',
- },
-];
-
-const fieldNameToLabel: Record = modalStructureWithoutPassword.reduce(
- (acc, curr) => {
- acc[curr.dataKey] = curr.label;
- return acc;
- },
- {} as Record,
-);
-
-const CreateUserModal: FC<{
+const CreateRoleModal: FC<{
modalOpen: boolean;
close: () => void;
}> = ({ modalOpen, close }) => {
const [form] = Form.useForm();
+ const router = useRouter();
const { message: messageApi } = App.useApp();
- type ErrorsObject = { [field in PostUserField]?: ReactNode[] };
+ type ErrorsObject = { [field in PostRoleKeys]?: ReactNode[] };
const [formatError, setFormatError] = useState({});
-
- const { mutateAsync: postUser, isLoading } = usePostAsset('/users', {
+ const { mutateAsync: postRole, isLoading } = usePostAsset('/roles', {
onError(e) {
if (!(typeof e === 'object' && e !== null && 'errors' in e)) {
return;
}
- const errors: { [key in PostUserField]?: ReactNode[] } = {};
-
- function appendError(key: PostUserField, error: string) {
- error = error.replace(key, fieldNameToLabel[key]);
+ const errors: { [key in PostRoleKeys]?: ReactNode[] } = {};
+ function appendError(key: PostRoleKeys, error: string) {
if (key in errors) {
errors[key]!.push({error}
);
} else {
@@ -71,102 +37,93 @@ const CreateUserModal: FC<{
}
for (const error of e.errors as string[]) {
- if (error.includes('username')) appendError('username', error);
- else if (error.includes('email')) appendError('email', error);
- else if (error.includes('firstName')) appendError('firstName', error);
- else if (error.includes('lastName')) appendError('lastName', error);
- else if (error.includes('password')) appendError('password', error);
+ if (error.includes('name')) appendError('name', error);
+ else if (error.includes('description')) appendError('description', error);
+ else if (error.includes('expiration')) appendError('expiration', error);
}
setFormatError(errors);
},
});
+ const [submittable, setSubmittable] = useState(false);
+ const values = Form.useWatch('name', form);
+
+ useEffect(() => {
+ form.validateFields({ validateOnly: true }).then(
+ () => {
+ setSubmittable(true);
+ },
+ () => {
+ setSubmittable(false);
+ },
+ );
+ }, [form, values]);
+
useEffect(() => {
form.resetFields();
setFormatError({});
}, [form, modalOpen]);
- const submitData = async (
- values: Record | 'confirm_password', string>,
- ) => {
- debugger;
+ const submitData = async (values: Record<'name' | 'description' | 'expirationDayJs', 'post'>) => {
+ let expiration;
+ if (typeof values.expirationDayJs === 'object')
+ expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString();
+
try {
- await postUser({
+ const newRole = await postRole({
body: {
- email: values.email,
- firstName: values.firstName,
- lastName: values.lastName,
- username: values.username,
- password: values.password,
+ name: values.name,
+ description: values.description,
+ expiration,
},
});
- messageApi.success({ content: 'Account created' });
- close();
+ messageApi.success({ content: 'Role created' });
+ router.push(`/iam/roles/${newRole.id}`);
} catch (e) {
- messageApi.error({ content: 'An error ocurred' });
+ messageApi.error({ content: 'Something went wrong' });
}
};
return (
-
+
-
-
- ))}
+
+
+
-
+
({
- validator(_, value) {
- if (!value || getFieldValue('password') === value) {
- return Promise.resolve();
- }
- return Promise.reject(new Error('The new password that you entered do not match!'));
- },
- }),
- ]}
+ label="Expiration"
+ name="expirationDayJs"
+ help={formatError.expiration}
+ validateStatus={formatError.expiration && 'error'}
>
-
+
-
- Submit
+
+ Create Role
@@ -175,18 +132,18 @@ const CreateUserModal: FC<{
};
const HeaderActions: FC = () => {
- const [createUserModalOpen, setCreateUserModalOpen] = useState(false);
+ const [createRoleModalOpen, setCreateRoleModalOpen] = useState(false);
return (
<>
- setCreateUserModalOpen(false)}
+ setCreateRoleModalOpen(false)}
/>
-
- setCreateUserModalOpen(true)}>
- Create
+
+ setCreateRoleModalOpen(true)}>
+ Create Role
>
diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx
index 519444dcc..aafad4618 100644
--- a/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/roles/role-page.tsx
@@ -1,10 +1,8 @@
'use client';
-import React, { FC, useState } from 'react';
-import styles from '@/components/processes.module.scss';
-import cn from 'classnames';
+import { FC, useState } from 'react';
import { DeleteOutlined } from '@ant-design/icons';
-import { Tooltip, Space, Row, Col, Button, Input, Result, Table, Popconfirm, App } from 'antd';
+import { Tooltip, Space, Button, Result, Table, Popconfirm, App } from 'antd';
import { useGetAsset, useDeleteAsset, ApiData } from '@/lib/fetch-data';
import { CloseOutlined } from '@ant-design/icons';
import Content from '@/components/content';
@@ -14,29 +12,33 @@ import Link from 'next/link';
import { toCaslResource } from '@/lib/ability/caslAbility';
import Bar from '@/components/bar';
import { AuthCan } from '@/lib/clientAuthComponents';
+import { useAbilityStore } from '@/lib/abilityStore';
+
type Role = ApiData<'/roles', 'get'>[number];
const RolesPage: FC = () => {
const { message: messageApi } = App.useApp();
-
+ const ability = useAbilityStore((store) => store.ability);
const { error, data: roles, isLoading, refetch: refetchRoles } = useGetAsset('/roles', {});
const { mutateAsync: deleteRole, isLoading: deletingRole } = useDeleteAsset('/roles/{id}', {
onSuccess: () => refetchRoles(),
onError: () => messageApi.open({ type: 'error', content: 'Something went wrong' }),
});
- const {
- searchQuery,
- setSearchQuery,
- filteredData: filteredRoles,
- } = useFuzySearch(roles || [], ['name'], {
+ const { setSearchQuery, filteredData: filteredRoles } = useFuzySearch(roles || [], ['name'], {
useSearchParams: false,
});
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [selectedRow, setSelectedRows] = useState([]);
+
+ const cannotDeleteSelected = selectedRow.some(
+ (role) => !ability.can('delete', toCaslResource('Role', role)),
+ );
async function deleteRoles(userIds: string[]) {
setSelectedRowKeys([]);
+ setSelectedRows([]);
await Promise.allSettled(userIds.map((id) => deleteRole({ params: { path: { id } } })));
}
@@ -63,8 +65,8 @@ const RolesPage: FC = () => {
deleteRoles([id])}
>
} type="text" />
@@ -86,51 +88,38 @@ const RolesPage: FC = () => {
return (
- } />
-
- 0 })}
- style={{
- justifyContent: 'start',
- }}
- >
- {selectedRowKeys.length > 0 ? (
+ }
+ leftNode={
+ selectedRowKeys.length > 0 ? (
} onClick={() => setSelectedRowKeys([])} />
- {selectedRowKeys.length} selected:
+ {selectedRowKeys.length} selected:
deleteRoles(selectedRowKeys)}
>
- } />
+ } disabled={cannotDeleteSelected} />
- ) : null}
-
-
- setSearchQuery(e.target.value)}
- allowClear
- placeholder="Search Users"
- />
-
-
+ ) : null
+ }
+ searchProps={{
+ onChange: (e) => setSearchQuery(e.target.value),
+ onPressEnter: (e) => setSearchQuery(e.currentTarget.value),
+ placeholder: 'Search Role ...',
+ }}
+ />
{
+ onChange: (selectedRowKeys, selectedRows) => {
setSelectedRowKeys(selectedRowKeys as string[]);
+ setSelectedRows(selectedRows);
},
}}
rowKey="id"
diff --git a/src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx b/src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx
index f7aad90e9..a6d9f3f63 100644
--- a/src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx
+++ b/src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx
@@ -47,10 +47,24 @@ const CreateUserModal: FC<{
modalOpen: boolean;
close: () => void;
}> = ({ modalOpen, close }) => {
- const [form] = Form.useForm();
const { message: messageApi } = App.useApp();
type ErrorsObject = { [field in PostUserField]?: ReactNode[] };
const [formatError, setFormatError] = useState({});
+ const [form] = Form.useForm();
+
+ const [submittable, setSubmittable] = useState(false);
+ const values = Form.useWatch([], form);
+
+ useEffect(() => {
+ form.validateFields({ validateOnly: true }).then(
+ () => {
+ setSubmittable(true);
+ },
+ () => {
+ setSubmittable(false);
+ },
+ );
+ }, [form, values]);
const { mutateAsync: postUser, isLoading } = usePostAsset('/users', {
onError(e) {
@@ -108,7 +122,7 @@ const CreateUserModal: FC<{
};
return (
-
+
-
- Submit
+
+ Create User
@@ -185,7 +199,7 @@ const HeaderActions: FC = () => {
setCreateUserModalOpen(true)}>
- Create
+ Create User
>
diff --git a/src/management-system-v2/components/layout.tsx b/src/management-system-v2/components/layout.tsx
index eb1cd6d5b..fc2bd241a 100644
--- a/src/management-system-v2/components/layout.tsx
+++ b/src/management-system-v2/components/layout.tsx
@@ -6,7 +6,7 @@ import { Layout as AntLayout, Grid, Menu } from 'antd';
const { Item, Divider, ItemGroup } = Menu;
import { SettingOutlined, ApiOutlined, UserOutlined, UnlockOutlined } from '@ant-design/icons';
import Image from 'next/image';
-import { usePathname } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import cn from 'classnames';
import { useAbilityStore } from '@/lib/abilityStore';
import Link from 'next/link';
@@ -24,6 +24,7 @@ import { useSession } from 'next-auth/react';
*/
const Layout: FC = ({ children }) => {
const activeSegment = usePathname().slice(1) || 'processes';
+ const router = useRouter();
const [collapsed, setCollapsed] = useState(false);
const { status } = useSession();
const loggedIn = status === 'authenticated';
@@ -78,7 +79,7 @@ const Layout: FC = ({ children }) => {
icon={}
hidden={!ability.can('manage', 'User')}
>
- Users
+ Users
- = ({ children }) => {
!(ability.can('manage', 'RoleMapping') || ability.can('manage', 'Role'))
}
>
- Roles
+ Roles
diff --git a/src/management-system-v2/lib/ability/caslAbility.ts b/src/management-system-v2/lib/ability/caslAbility.ts
index 98a4d2c32..1b8e9a0c0 100644
--- a/src/management-system-v2/lib/ability/caslAbility.ts
+++ b/src/management-system-v2/lib/ability/caslAbility.ts
@@ -188,5 +188,5 @@ export function toCaslResource>(
resource: ResourceType,
object: T,
) {
- return subject(resource, object);
+ return subject(resource, { ...object });
}
diff --git a/src/management-system-v2/lib/openapiSchema.ts b/src/management-system-v2/lib/openapiSchema.ts
index 7d1c738d5..6c3a06b54 100644
--- a/src/management-system-v2/lib/openapiSchema.ts
+++ b/src/management-system-v2/lib/openapiSchema.ts
@@ -372,8 +372,8 @@ export interface components {
* - ezample 2
*/
PermissionNumber: number;
- /** role */
- roleData: {
+ /** rolePostData */
+ rolePostData: {
name?: string;
description?: string;
note?: string;
@@ -391,6 +391,9 @@ export interface components {
All?: components['schemas']['PermissionNumber'];
};
expiration?: string;
+ };
+ /** role */
+ roleData: {
members?: {
userId: string;
username: string;
@@ -399,7 +402,7 @@ export interface components {
email: string;
}[];
default?: boolean;
- };
+ } & components['schemas']['rolePostData'];
/** role */
roleMetaData: {
id?: string;
@@ -1198,10 +1201,7 @@ export interface operations {
postRole: {
requestBody?: {
content: {
- 'application/json': WithRequired<
- components['schemas']['roleData'],
- 'name' | 'description' | 'note' | 'permissions' | 'expiration' | 'members' | 'default'
- >;
+ 'application/json': WithRequired;
};
};
responses: {
diff --git a/src/management-system-v2/lib/useFuzySearch.ts b/src/management-system-v2/lib/useFuzySearch.ts
index 36b27db46..aa1495795 100644
--- a/src/management-system-v2/lib/useFuzySearch.ts
+++ b/src/management-system-v2/lib/useFuzySearch.ts
@@ -22,8 +22,7 @@ export default function useFuzySearch>(
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useState(
- searchParams.get(fuzzySearchOptions.useSearchParams ? fuzzySearchOptions.queryName : '') ??
- undefined,
+ searchParams.get(fuzzySearchOptions.useSearchParams ? fuzzySearchOptions.queryName : '') ?? '',
);
const debouncedSearchQuery = useDebounce(searchQuery, 200, fuzzySearchOptions.useSearchParams);
diff --git a/src/management-system/src/backend/openapi.json b/src/management-system/src/backend/openapi.json
index 13ac91b0b..c5337ccf4 100644
--- a/src/management-system/src/backend/openapi.json
+++ b/src/management-system/src/backend/openapi.json
@@ -1208,18 +1208,10 @@
"schema": {
"allOf": [
{
- "$ref": "#/components/schemas/roleData"
+ "$ref": "#/components/schemas/rolePostData"
}
],
- "required": [
- "name",
- "description",
- "note",
- "permissions",
- "expiration",
- "members",
- "default"
- ]
+ "required": ["name"]
}
}
}
@@ -2122,8 +2114,8 @@
"type": "number",
"description": "- example\n- ezample 2"
},
- "roleData": {
- "title": "role",
+ "rolePostData": {
+ "title": "rolePostData",
"type": "object",
"properties": {
"name": {
@@ -2175,35 +2167,48 @@
},
"expiration": {
"type": "string"
- },
- "members": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "userId": {
- "type": "string"
- },
- "username": {
- "type": "string"
- },
- "firstName": {
- "type": "string"
- },
- "lastName": {
- "type": "string"
- },
- "email": {
- "type": "string"
+ }
+ }
+ },
+ "roleData": {
+ "title": "role",
+ "allOf": [
+ {
+ "type": "object",
+ "properties": {
+ "members": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "userId": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ },
+ "required": ["userId", "username", "firstName", "lastName", "email"]
}
},
- "required": ["userId", "username", "firstName", "lastName", "email"]
+ "default": {
+ "type": "boolean"
+ }
}
},
- "default": {
- "type": "boolean"
+ {
+ "$ref": "#/components/schemas/rolePostData"
}
- }
+ ]
},
"roleMetaData": {
"title": "role",
diff --git a/src/management-system/src/backend/server/iam/rest-api/roles.js b/src/management-system/src/backend/server/iam/rest-api/roles.js
index c01912eec..f624fb6b0 100644
--- a/src/management-system/src/backend/server/iam/rest-api/roles.js
+++ b/src/management-system/src/backend/server/iam/rest-api/roles.js
@@ -99,10 +99,8 @@ rolesRouter.put('/:id', validateRole, isAllowed('update', 'Role'), async (req, r
const targetRole = getRoleById(id);
- for (const [key, value] of Object.entries(role)) {
- console.log('Key:', key, 'Value:', value, 'Target:', targetRole[key]);
+ for (const [key, value] of Object.entries(role))
if (targetRole[key] === value) delete role[key];
- }
// Casl isn't really built to check the value of input fields when updating, so we have to perform this two checks
if (
@@ -144,7 +142,7 @@ rolesRouter.delete('/:id', isAllowed('delete', 'Role'), async (req, res) => {
/** @type {Ability} */
const userAbility = req.userAbility;
- if (!userAbility.can('update', toCaslResource('Role', role)))
+ if (!userAbility.can('delete', toCaslResource('Role', role)))
return res.status(403).send('Forbidden.');
await deleteRole(id);
diff --git a/src/management-system/src/backend/shared-electron-server/data/iam/roles.js b/src/management-system/src/backend/shared-electron-server/data/iam/roles.js
index cd6aaaecb..5aa144ccf 100644
--- a/src/management-system/src/backend/shared-electron-server/data/iam/roles.js
+++ b/src/management-system/src/backend/shared-electron-server/data/iam/roles.js
@@ -45,7 +45,7 @@ export async function addRole(roleRepresentation) {
name,
description: description || null,
note: note || null,
- permissions: permissions || null,
+ permissions: permissions || {},
expiration: expiration || null,
members: [],
id,