Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ms2/iam interface #137

Merged
merged 52 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
792d393
Fixed and Updated OpenApi and changed users endpoint
FelipeTrost Sep 29, 2023
cd30d33
Data hooks: added MutationOptions and changed keys
FelipeTrost Sep 29, 2023
63f5c61
Added Users page and Comming Soon page for roles
FelipeTrost Sep 29, 2023
ee745a0
Added useFuzySearch hook
FelipeTrost Sep 30, 2023
098ff11
Typo
FelipeTrost Sep 30, 2023
247cf3d
Merge branch 'main' into ms2/iam-interface
FelipeTrost Sep 30, 2023
1d71149
Merge branch 'main' into ms2/iam-interface
FelipeTrost Sep 30, 2023
9ed5851
Merge branch 'ms2/iam-interface' of github.com:PROCEED-Labs/proceed i…
FelipeTrost Sep 30, 2023
4e8c219
Removed submodule files
FelipeTrost Sep 30, 2023
1add266
Removed 204 response for roles
FelipeTrost Oct 6, 2023
911389f
Fix: cannot delete default roles
FelipeTrost Oct 6, 2023
e275685
Merge main into ms2/iam-interface
FelipeTrost Oct 6, 2023
47242a6
Moved toCaslResources to ms-v2
FelipeTrost Oct 9, 2023
fe81a29
Merged main into ms2/iam-interface
FelipeTrost Oct 10, 2023
b8b75ff
Removed debugger statement
FelipeTrost Oct 12, 2023
0c3b3a9
PUT /role now uses only fields that changed for auth
FelipeTrost Oct 12, 2023
e56aa78
Updated authorization rules
FelipeTrost Oct 12, 2023
9d3d303
Fixed API endpoints in OpenAPI description
FelipeTrost Oct 12, 2023
a169c8f
Refactor and implemented useInvalidateAsseta
FelipeTrost Oct 12, 2023
26c7ae2
Updated subject type to accept objects
FelipeTrost Oct 12, 2023
709cf6f
Moved AuthCallbackListener to be inside of AntDesign context
FelipeTrost Oct 12, 2023
1df00d5
Updated active segment logic
FelipeTrost Oct 12, 2023
8b3e0c4
Moved user list to a separate component
FelipeTrost Oct 12, 2023
7696a83
Implemented role pages
FelipeTrost Oct 12, 2023
d56a52c
Removed console.log
FelipeTrost Oct 16, 2023
d0e8c76
Fixed permission check in api
FelipeTrost Oct 16, 2023
7fd6759
Use custom Bar component
FelipeTrost Oct 16, 2023
e717b20
Updated POST: /roles endpoint in openapi
FelipeTrost Oct 16, 2023
4046c02
Default permissions value is now an empty object
FelipeTrost Oct 16, 2023
330c08f
Roles page header actions
FelipeTrost Oct 16, 2023
d9f1d68
Cleaned up imports
FelipeTrost Oct 16, 2023
d230f25
Changed texts and updated ui
FelipeTrost Oct 16, 2023
f64e6e6
Changed order of tabs
FelipeTrost Oct 16, 2023
bbc5c95
Changed roleGeneralData style
FelipeTrost Oct 16, 2023
06df5a0
merged 'main' into 'ms2/iam-interface'
FelipeTrost Oct 23, 2023
11efff0
toCaslResource creates a shallow copy
FelipeTrost Oct 23, 2023
2a183c7
Update openapi typescript types
FelipeTrost Oct 25, 2023
c61ac6d
Fix typo
FelipeTrost Oct 25, 2023
27b9720
use Bar + check if selected items can be deleted
FelipeTrost Oct 25, 2023
90700a3
Create role modal
FelipeTrost Oct 25, 2023
27bbfd7
Corrected confirmation popup text
FelipeTrost Oct 25, 2023
c9b378d
Rearange tabs in role page
FelipeTrost Oct 26, 2023
613c7ec
Merge 'origin/main' into ms2/iam-interface
FelipeTrost Oct 26, 2023
5db7d33
fixed openapi POST /role types
FelipeTrost Oct 26, 2023
5a99e57
Removed console log
FelipeTrost Oct 26, 2023
c233c7d
Fixed permission check
FelipeTrost Oct 26, 2023
43a8160
Disable button instead of hidding it
FelipeTrost Oct 26, 2023
d08a902
Moved skelleton to the inside of the container
FelipeTrost Nov 5, 2023
b17a3e1
Updated texts and form validation
FelipeTrost Nov 5, 2023
28a220a
Added Link component to sidebr items
FelipeTrost Nov 5, 2023
ce5d49a
searchQury defaults to empty string
FelipeTrost Nov 11, 2023
a2b48a0
Merge remote-tracking branch 'origin/main' into ms2/iam-interface
FelipeTrost Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ const RoleMembers: FC<{ role: Role; isLoadingRole?: boolean }> = ({ role, isLoad
)}
searchBarRightNode={
<Button type="primary" onClick={() => setAddUserModalOpen(true)}>
Add member
Add Member
</Button>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ function RolePage({ params: { roleId } }: { params: { roleId: string } }) {

const items: Items = role
? [
{
key: 'members',
label: 'Manage Members',
children: <RoleMembers role={role} isLoadingRole={isLoading} />,
},
{ key: 'permissions', label: 'Permissions', children: <RolePermissions role={role} /> },
{
key: 'generalData',
label: 'General Data',
children: <RoleGeneralData roleId={roleId} />,
},
{ key: 'permissions', label: 'Permissions', children: <RolePermissions role={role} /> },
{
key: 'members',
label: 'Manage Members',
children: <RoleMembers role={role} isLoadingRole={isLoading} />,
},
]
: [];

Expand All @@ -58,9 +58,11 @@ function RolePage({ params: { roleId } }: { params: { roleId: string } }) {
</Space>
}
>
<Skeleton loading={isLoading}>
<Tabs items={items} />
</Skeleton>
<div style={{ maxWidth: '800px', margin: 'auto' }}>
<Skeleton loading={isLoading}>
<Tabs items={items} />
</Skeleton>
</div>
</Content>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 <Spin />;

const role = toCaslResource('Role', data);
Expand Down Expand Up @@ -51,7 +65,13 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
<br />
</>
)}
<Form.Item label="Name" name="name">

<Form.Item
label="Name"
name="name"
rules={[{ required: true, message: 'this field is required' }]}
required
>
<Input placeholder="input placeholder" disabled={!ability.can('update', role, 'name')} />
</Form.Item>

Expand All @@ -73,8 +93,8 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
</Form.Item>

<Form.Item>
<Button type="primary" htmlType="submit" loading={putLoading}>
Submit
<Button type="primary" htmlType="submit" loading={putLoading} disabled={!submittable}>
Update Role
</Button>
</Form.Item>
</Form>
Expand Down
197 changes: 77 additions & 120 deletions src/management-system-v2/app/(dashboard)/iam/roles/header-actions.tsx
Original file line number Diff line number Diff line change
@@ -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<PostUserField, string> = modalStructureWithoutPassword.reduce(
(acc, curr) => {
acc[curr.dataKey] = curr.label;
return acc;
},
{} as Record<PostUserField, string>,
);

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<ErrorsObject>({});

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(<p key={errors[key]?.length}>{error}</p>);
} else {
Expand All @@ -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<keyof ApiRequestBody<'/users', 'post'> | '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 (
<Modal open={modalOpen} onCancel={close} footer={null}>
<Modal open={modalOpen} onCancel={close} footer={null} title="Create New Role">
<Form form={form} layout="vertical" onFinish={submitData}>
{modalStructureWithoutPassword.map((formField) => (
<Form.Item
key={formField.dataKey}
label={formField.label}
name={formField.dataKey}
validateStatus={formField.dataKey in formatError ? 'error' : ''}
help={formField.dataKey in formatError ? formatError[formField.dataKey] : ''}
hasFeedback
required
>
<Input type={formField.type} />
</Form.Item>
))}
<Form.Item
label="Name"
name="name"
help={formatError.name}
validateStatus={formatError.name && 'error'}
rules={[{ required: true, message: 'This field is required' }]}
required
>
<Input />
</Form.Item>

<Form.Item
name="password"
label="Password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
validateStatus={'password' in formatError ? 'error' : ''}
help={'password' in formatError ? formatError.password : ''}
hasFeedback
label="Description"
name="description"
help={formatError.description}
validateStatus={formatError.description && 'error'}
>
<Input.Password />
<Input.TextArea />
</Form.Item>

<Form.Item
name="confirm_password"
label="Confirm Password"
dependencies={['password']}
hasFeedback
rules={[
{
required: true,
message: 'Please confirm your password!',
},
({ getFieldValue }) => ({
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'}
>
<Input.Password />
<DatePicker
// NOTE german locale hard coded
locale={germanLocale}
allowClear={true}
/>
</Form.Item>

<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoading}>
Submit
<Button type="primary" htmlType="submit" loading={isLoading} disabled={!submittable}>
Create Role
</Button>
</Form.Item>
</Form>
Expand All @@ -175,18 +132,18 @@ const CreateUserModal: FC<{
};

const HeaderActions: FC = () => {
const [createUserModalOpen, setCreateUserModalOpen] = useState(false);
const [createRoleModalOpen, setCreateRoleModalOpen] = useState(false);

return (
<>
<CreateUserModal
modalOpen={createUserModalOpen}
close={() => setCreateUserModalOpen(false)}
<CreateRoleModal
modalOpen={createRoleModalOpen}
close={() => setCreateRoleModalOpen(false)}
/>

<AuthCan action="create" resource="User">
<Button type="primary" onClick={() => setCreateUserModalOpen(true)}>
<PlusOutlined /> Create
<AuthCan action="create" resource="Role">
<Button type="primary" onClick={() => setCreateRoleModalOpen(true)}>
<PlusOutlined /> Create Role
</Button>
</AuthCan>
</>
Expand Down
Loading