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/replace auth0 #210

Merged
merged 35 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ad3735e
Use null instead of undefined
FelipeTrost Dec 11, 2023
8975a47
Changed ability and csrf setup
FelipeTrost Dec 11, 2023
58a6b25
Changed redis for lru-cache
FelipeTrost Dec 11, 2023
d9666ce
Fix: Admin role was being added twice
FelipeTrost Dec 13, 2023
a138d7e
Used .global for stores and moved init functions
FelipeTrost Dec 13, 2023
6874f88
Bug fix: Authcan redirected when loading
FelipeTrost Dec 13, 2023
b6c31dd
Ability filter generic
FelipeTrost Dec 13, 2023
2e4bab4
Bug fix: Ability filter recognize null values
FelipeTrost Dec 13, 2023
215f006
Unauthorized error class
FelipeTrost Dec 13, 2023
df81a65
Added permissions check and changed init
FelipeTrost Dec 13, 2023
e489f8c
Server actions for role-mappings and roles
FelipeTrost Dec 13, 2023
01155c7
Switched components to rsc and to server actions
FelipeTrost Dec 13, 2023
9a2dad5
loading page and error hanlers
FelipeTrost Dec 13, 2023
4433c05
Fixed permissions check
FelipeTrost Dec 15, 2023
4cb7867
Merge remote-tracking branch 'origin/main' into move-auth-to-ms2
FelipeTrost Dec 15, 2023
8bbfb1e
Ms2: Role and Role-Mapping migrations
FelipeTrost Dec 17, 2023
8617c0f
Fix: Dev admin should have admin role
FelipeTrost Dec 18, 2023
cd2b4bb
Remove debugger calls
FelipeTrost Dec 18, 2023
7dc213f
Update page when role changes
FelipeTrost Dec 18, 2023
f369c37
Removed console.log
FelipeTrost Dec 20, 2023
b551b07
Removed unneeded asyncs and cleaned imports
FelipeTrost Dec 20, 2023
e83d172
Implemented userError
FelipeTrost Dec 20, 2023
31c5301
Remove unnecessary dynamic
FelipeTrost Dec 20, 2023
0bc4cab
Ms2: installed zod
FelipeTrost Dec 23, 2023
0caff8a
Merge branch 'move-auth-to-ms2' into ms2/replace-auth0
FelipeTrost Dec 23, 2023
c236539
ts util: WithRequired
FelipeTrost Jan 2, 2024
0358ce4
Feat: User store with Zod validation
FelipeTrost Jan 2, 2024
c9e5d27
Fix UnauthorizedError parameter type
FelipeTrost Jan 2, 2024
2c5c29d
Moved userSchema to sepparate file + removed sleep
FelipeTrost Jan 2, 2024
64062d0
Implemented user store in the UI
FelipeTrost Jan 2, 2024
4c2472a
Check if user's email or username already exist
FelipeTrost Jan 2, 2024
8a3118d
Update role-mappings
FelipeTrost Jan 2, 2024
192b848
Create user entries on signUp and signIn + namespace ids
FelipeTrost Jan 2, 2024
0a17260
Merge remote-tracking branch 'origin/main' into ms2/replace-auth0
FelipeTrost Jan 2, 2024
06d1f9a
Removed console log
FelipeTrost Jan 2, 2024
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
141 changes: 31 additions & 110 deletions src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
'use client';

import { AuthCan } from '@/components/auth-can';
import { ApiRequestBody, usePostAsset } from '@/lib/fetch-data';
import { UserSchema } from '@/lib/data/user-schema';
import { addUser } from '@/lib/data/users';
import useParseZodErrors from '@/lib/useParseZodErrors';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, App, Input, Modal } from 'antd';
import { FC, ReactNode, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { FC, useEffect, useState, useTransition } from 'react';

type PostUserField = keyof ApiRequestBody<'/users', 'post'>;

const modalStructureWithoutPassword: {
dataKey: PostUserField;
label: string;
type: string;
}[] = [
const modalStructureWithoutPassword = [
{
dataKey: 'firstName',
label: 'First Name',
Expand All @@ -33,24 +30,17 @@ const modalStructureWithoutPassword: {
label: 'Email',
type: 'email',
},
];

const fieldNameToLabel: Record<PostUserField, string> = modalStructureWithoutPassword.reduce(
(acc, curr) => {
acc[curr.dataKey] = curr.label;
return acc;
},
{} as Record<PostUserField, string>,
);
] as const;

const CreateUserModal: FC<{
modalOpen: boolean;
close: () => void;
}> = ({ modalOpen, close }) => {
const { message: messageApi } = App.useApp();
type ErrorsObject = { [field in PostUserField]?: ReactNode[] };
const [formatError, setFormatError] = useState<ErrorsObject>({});
const router = useRouter();

const [form] = Form.useForm();
const [formErrors, parseInput] = useParseZodErrors(UserSchema);

const [submittable, setSubmittable] = useState(false);
const values = Form.useWatch([], form);
Expand All @@ -66,59 +56,30 @@ const CreateUserModal: FC<{
);
}, [form, values]);

const { mutateAsync: postUser, isLoading } = usePostAsset('/users', {
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]);

if (key in errors) {
errors[key]!.push(<p key={errors[key]?.length}>{error}</p>);
} else {
errors[key] = [<p key={0}>{error}</p>];
}
}

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);
}

setFormatError(errors);
},
});
const [isLoading, startTransition] = useTransition();

useEffect(() => {
form.resetFields();
setFormatError({});
}, [form, modalOpen]);

const submitData = async (
values: Record<keyof ApiRequestBody<'/users', 'post'> | 'confirm_password', string>,
) => {
try {
await postUser({
body: {
email: values.email,
firstName: values.firstName,
lastName: values.lastName,
username: values.username,
password: values.password,
},
});
messageApi.success({ content: 'Account created' });
close();
} catch (e) {
messageApi.error({ content: 'An error ocurred' });
}
const submitData = (values: any) => {
startTransition(async () => {
try {
form.validateFields();

const data = parseInput({ ...values, oauthProvider: 'email' });
if (!data) return;

const result = await addUser(data);
if (result && 'error' in result) throw new Error();

messageApi.success({ content: 'Account created' });
router.refresh();
close();
} catch (e) {
messageApi.error({ content: 'An error ocurred' });
}
});
};

return (
Expand All @@ -129,54 +90,15 @@ const CreateUserModal: FC<{
key={formField.dataKey}
label={formField.label}
name={formField.dataKey}
validateStatus={formField.dataKey in formatError ? 'error' : ''}
help={formField.dataKey in formatError ? formatError[formField.dataKey] : ''}
validateStatus={formField.dataKey in formErrors ? 'error' : ''}
help={formField.dataKey in formErrors ? formErrors[formField.dataKey] : ''}
hasFeedback
required
>
<Input type={formField.type} />
</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
>
<Input.Password />
</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!'));
},
}),
]}
>
<Input.Password />
</Form.Item>

<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoading} disabled={!submittable}>
Create User
Expand All @@ -196,7 +118,6 @@ const HeaderActions: FC = () => {
modalOpen={createUserModalOpen}
close={() => setCreateUserModalOpen(false)}
/>

<AuthCan action="create" resource="User">
<Button type="primary" onClick={() => setCreateUserModalOpen(true)}>
<PlusOutlined /> Create User
Expand Down
17 changes: 15 additions & 2 deletions src/management-system-v2/app/(dashboard)/iam/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import Auth from '@/components/auth';
import Auth, { getCurrentUser } from '@/components/auth';
import UsersPage from './users-page';
import { getUsers } from '@/lib/data/legacy/iam/users';
import Content from '@/components/content';

const Page = async () => {
const { ability } = await getCurrentUser();
const users = getUsers(ability);

return (
<Content title="Identity and Access Management">
<UsersPage users={users} />
</Content>
);
};

export default Auth(
{
action: 'manage',
resource: 'User',
fallbackRedirect: '/',
},
UsersPage,
Page,
);
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ const UserSidePanel: FC<{ user: ListUser | null }> = ({ user }) => {
>
{user ? (
<>
<Avatar src={user.picture} size={60} style={{ marginBottom: 20 }}>
{user.picture
<Avatar src={user.image} size={60} style={{ marginBottom: 20 }}>
{user.image
? null
: user.firstName.value.slice(0, 1) + user.lastName.value.slice(0, 1)}
</Avatar>
Expand Down
120 changes: 59 additions & 61 deletions src/management-system-v2/app/(dashboard)/iam/users/users-page.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,78 @@
'use client';

import { FC, useState } from 'react';
import { FC, useState, useTransition } from 'react';
import { DeleteOutlined } from '@ant-design/icons';
import { Tooltip, App } from 'antd';
import { useGetAsset, useDeleteAsset } from '@/lib/fetch-data';
import Content from '@/components/content';
import HeaderActions from './header-actions';
import UserList, { ListUser } from '@/components/user-list';
import { useQueryClient } from '@tanstack/react-query';
import ConfirmationButton from '@/components/confirmation-button';
import UserSidePanel from './user-side-panel';
import { deleteUsers as serverActionDeleteUsers } from '@/lib/data/users';
import { useRouter } from 'next/navigation';
import { User } from '@/lib/data/user-schema';

const UsersPage: FC = () => {
const UsersPage: FC<{ users: User[] }> = ({ users }) => {
const { message: messageApi } = App.useApp();
const queryClient = useQueryClient();
const [selectedUser, setSelectedUser] = useState<ListUser | null>(null);
const [deletingUser, startTransition] = useTransition();
const router = useRouter();

const { error, data, isLoading } = useGetAsset('/users', {});
const { mutateAsync: deleteUser, isLoading: deletingUser } = useDeleteAsset('/users/{id}', {
onError: () => messageApi.open({ type: 'error', content: 'Something went wrong' }),
onSuccess: async () => await queryClient.invalidateQueries(['/users']),
});
async function deleteUsers(ids: string[], unsetIds: () => void) {
startTransition(async () => {
unsetIds();

async function deleteUsers(ids: string[], unsetIds?: () => void) {
if (unsetIds) unsetIds();
const promises = ids.map((id) => deleteUser({ params: { path: { id } } }));
await Promise.allSettled(promises);
const result = await serverActionDeleteUsers(ids);

if (result && 'error' in result)
messageApi.open({ type: 'error', content: 'Something went wrong' });

router.refresh();
});
}

return (
<Content title="Identity and Access Management">
<UserList
users={data || []}
error={!!error}
columns={(clearSelected, hoveredId, selectedRowKeys) => [
{
dataIndex: 'id',
key: 'tooltip',
title: '',
width: 100,
render: (id: string) => (
<Tooltip placement="top" title="Delete">
<ConfirmationButton
title="Delete User"
description="Are you sure you want to delete this user?"
onConfirm={() => deleteUsers([id], clearSelected)}
buttonProps={{
icon: <DeleteOutlined />,
type: 'text',
style: { opacity: hoveredId === id && selectedRowKeys.length === 0 ? 1 : 0 },
}}
/>
</Tooltip>
),
},
]}
loading={deletingUser || isLoading}
selectedRowActions={(ids, clearIds) => (
<ConfirmationButton
title="Delete Users"
description="Are you sure you want to delete the selected users?"
onConfirm={() => deleteUsers(ids, clearIds)}
buttonProps={{
type: 'text',
icon: <DeleteOutlined />,
}}
/>
)}
searchBarRightNode={<HeaderActions />}
onSelectedRows={(users) => {
console.log(users);
setSelectedUser(users.length > 0 ? users[users.length - 1] : null);
}}
sidePanel={<UserSidePanel user={selectedUser} />}
/>
</Content>
<UserList
users={users}
columns={(clearSelected, hoveredId, selectedRowKeys) => [
{
dataIndex: 'id',
key: 'tooltip',
title: '',
width: 100,
render: (id: string) => (
<Tooltip placement="top" title="Delete">
<ConfirmationButton
title="Delete User"
description="Are you sure you want to delete this user?"
onConfirm={() => deleteUsers([id], clearSelected)}
buttonProps={{
icon: <DeleteOutlined />,
type: 'text',
style: { opacity: hoveredId === id && selectedRowKeys.length === 0 ? 1 : 0 },
}}
/>
</Tooltip>
),
},
]}
loading={deletingUser}
selectedRowActions={(ids, clearIds) => (
<ConfirmationButton
title="Delete Users"
description="Are you sure you want to delete the selected users?"
onConfirm={() => deleteUsers(ids, clearIds)}
buttonProps={{
type: 'text',
icon: <DeleteOutlined />,
}}
/>
)}
searchBarRightNode={<HeaderActions />}
onSelectedRows={(users) => {
setSelectedUser(users.length > 0 ? users[users.length - 1] : null);
}}
sidePanel={<UserSidePanel user={selectedUser} />}
/>
);
};

Expand Down
Loading