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 #118

Merged
merged 24 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
@@ -0,0 +1,74 @@
'use client';

import Content from '@/components/content';
import Auth from '@/lib/AuthCanWrapper';
import { useGetAsset } from '@/lib/fetch-data';
import { Button, Result, Skeleton, Space, Tabs } from 'antd';
import { LeftOutlined } from '@ant-design/icons';
import { ComponentProps } from 'react';
import RoleGeneralData from './roleGeneralData';
import { useRouter } from 'next/navigation';
import RolePermissions from './rolePermissions';
import RoleMembers from './role-members';

type Items = ComponentProps<typeof Tabs>['items'];

function RolePage({ params: { roleId } }: { params: { roleId: string } }) {
const router = useRouter();
const {
data: role,
isLoading,
error,
} = useGetAsset('/roles/{id}', {
params: { path: { id: roleId } },
});

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} />,
},
]
: [];

if (error) return;
<Result
status="error"
title="Failed to fetch role"
subTitle="An error ocurred while fetching role, please try again."
/>;

return (
<Content
title={
<Space>
<Button icon={<LeftOutlined />} onClick={() => router.push('/iam/roles')} type="text">
Back
</Button>
{role?.name}
</Space>
}
>
<Skeleton loading={isLoading}>
<Tabs items={items} />
</Skeleton>
</Content>
);
}

export default Auth(
{
action: ['view', 'manage'],
resource: 'Role',
fallbackRedirect: '/',
},
RolePage,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
'use client';

import { FC, useMemo, useState } from 'react';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import {
ApiData,
useDeleteAsset,
useGetAsset,
useInvalidateAsset,
usePostAsset,
} from '@/lib/fetch-data';
import UserList, { UserListProps } from '@/components/user-list';
import { Button, Modal, Popconfirm, Tooltip } from 'antd';

type Role = ApiData<'/roles', 'get'>[number];

const AddUserModal: FC<{ role: Role; open: boolean; close: () => void }> = ({
role,
open,
close,
}) => {
const { data: users, isLoading: isLoadingUsers } = useGetAsset('/users', {});
const invalidateRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } });
const { mutateAsync, isLoading: isLoadingMutation } = usePostAsset('/role-mappings', {
onSuccess: invalidateRole,
});

type AddUserParams = Parameters<NonNullable<UserListProps['selectedRowActions']>>;
const addUsers = async (users: AddUserParams[2], clearIds?: AddUserParams[1]) => {
if (clearIds) clearIds();
await mutateAsync({
body: users.map((user) => ({
userId: user.id,
roleId: role.id,
email: user.email,
lastName: user.lastName,
firstName: user.firstName,
username: user.username,
})),
parseAs: 'text',
});
};

const usersNotInRole = useMemo(() => {
if (!users) return [];

const usersInRole = new Set(role.members.map((member) => member.userId));

return users.filter((user) => !usersInRole.has(user.id));
}, [users, role.members]);

return (
<Modal
open={open}
onCancel={close}
footer={null}
width={800}
title={`Add members to ${role.name}`}
>
<UserList
users={usersNotInRole}
loading={isLoadingUsers || isLoadingMutation}
columns={(clearSelected) => [
{
dataIndex: 'id',
render: (_, user) => (
<Tooltip placement="top" title="Add to role">
<Button
icon={<PlusOutlined />}
type="text"
onClick={() => addUsers([user], clearSelected)}
/>
</Tooltip>
),
},
]}
selectedRowActions={(_, clearIds, users) => (
<Tooltip placement="top" title="Add to role">
<Button icon={<PlusOutlined />} type="text" onClick={() => addUsers(users, clearIds)} />
</Tooltip>
)}
/>
</Modal>
);
};

const RoleMembers: FC<{ role: Role; isLoadingRole?: boolean }> = ({ role, isLoadingRole }) => {
const [addUserModalOpen, setAddUserModalOpen] = useState(false);

const refetchRole = useInvalidateAsset('/roles/{id}', { params: { path: { id: role.id } } });
const { mutateAsync: deleteUser, isLoading: isLoadingDelete } = useDeleteAsset(
'/role-mappings/users/{userId}/roles/{roleId}',
{ onSuccess: refetchRole },
);

async function deleteMembers(userIds: string[], clearIds?: () => void) {
if (clearIds) clearIds();

await Promise.allSettled(
userIds.map((userId) =>
deleteUser({
params: { path: { roleId: role.id, userId: userId } },
}),
),
);
}

return (
<>
<AddUserModal role={role} open={addUserModalOpen} close={() => setAddUserModalOpen(false)} />
<UserList
users={role.members.map((member) => ({ ...member, id: member.userId }))}
loading={isLoadingDelete || isLoadingRole}
columns={(clearSelected) => [
{
dataIndex: 'id',
key: 'remove',
title: '',
width: 100,
render: (id: string) => (
<Tooltip placement="top" title="Remove member">
<Popconfirm
title="Remove member"
description="Are you sure you want to remove this member?"
onConfirm={() => deleteMembers([id], clearSelected)}
>
<Button icon={<DeleteOutlined />} type="text" />
</Popconfirm>
</Tooltip>
),
},
]}
selectedRowActions={(ids, clearIds) => (
<Tooltip placement="top" title="Remove members">
<Popconfirm
title="Remove member"
description="Are you sure you want to remove this member?"
onConfirm={() => deleteMembers(ids, clearIds)}
>
<Button icon={<DeleteOutlined />} type="text" />
</Popconfirm>
</Tooltip>
)}
searchBarRightNode={
<Button type="primary" onClick={() => setAddUserModalOpen(true)}>
Add member
</Button>
}
/>
</>
);
};

export default RoleMembers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Auth from '@/lib/AuthCanWrapper';
import { toCaslResource } from '@/lib/ability/caslAbility';
import { useGetAsset, usePutAsset } from '@/lib/fetch-data';
import { useAuthStore } from '@/lib/iam';
import { Alert, App, Button, DatePicker, Form, Input, Spin } from 'antd';
import { FC } from 'react';
import dayjs from 'dayjs';
import germanLocale from 'antd/es/date-picker/locale/de_DE';

const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
const { message } = App.useApp();
const ability = useAuthStore((store) => store.ability);
const [form] = Form.useForm();

const { data, isLoading, error } = useGetAsset('/roles/{id}', {
params: { path: { id: roleId } },
});

const { mutateAsync: updateRole, isLoading: putLoading } = usePutAsset('/roles/{id}', {
onError: () => message.open({ type: 'error', content: 'Something went wrong' }),
});

if (isLoading || error || !data) return <Spin />;

const role = toCaslResource('Role', data);

async function submitChanges(values: Record<string, any>) {
if (typeof values.expirationDayJs === 'object') {
values.expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString();
delete values.expirationDayJs;
}

try {
await updateRole({
params: { path: { id: roleId } },
body: values,
});

// success message has to go here, or else the mutation will stop loading when the message
// disappears
message.open({ type: 'success', content: 'Role updated' });
} catch (e) {}
}

return (
<Form form={form} layout="vertical" onFinish={submitChanges} initialValues={role}>
{role.note && (
<>
<Alert type="warning" message={role.note} />
<br />
</>
)}
<Form.Item label="Name" name="name">
<Input placeholder="input placeholder" disabled={!ability.can('update', role, 'name')} />
</Form.Item>

<Form.Item label="Description" name="description">
<Input.TextArea
placeholder="input placeholder"
disabled={!ability.can('update', role, 'description')}
/>
</Form.Item>

<Form.Item label="Expiration" name="expirationDayJs">
<DatePicker
// Note german locale hard coded
locale={germanLocale}
allowClear={true}
disabled={!ability.can('update', role, 'expiration')}
defaultValue={role.expiration ? dayjs(new Date(role.expiration)) : undefined}
/>
</Form.Item>

<Form.Item>
<Button type="primary" htmlType="submit" loading={putLoading}>
Submit
</Button>
</Form.Item>
</Form>
);
};

export default Auth(
{
action: ['view', 'manage'],
resource: 'Role',
fallbackRedirect: '/',
},
RoleGeneralData,
);
Loading