From 792d39395cd19fbca9703d7c2d91bdb43cbc7b3b Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 29 Sep 2023 15:50:43 +0200 Subject: [PATCH 01/44] Fixed and Updated OpenApi and changed users endpoint --- .../components/userProfile.tsx | 14 ++--- src/management-system-v2/lib/openapiSchema.ts | 34 ++++++------ .../src/backend/openapi.json | 53 ++++++++++--------- .../backend/server/iam/rest-api/auth0/user.js | 32 +++++------ 4 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/management-system-v2/components/userProfile.tsx b/src/management-system-v2/components/userProfile.tsx index bf3b3b646..c96e13621 100644 --- a/src/management-system-v2/components/userProfile.tsx +++ b/src/management-system-v2/components/userProfile.tsx @@ -83,8 +83,8 @@ const UserDataModal: FC<{ } else { const body = { email: userData.email, - firstName: userData.given_name, - lastName: userData.family_name, + firstName: userData.firstName, + lastName: userData.lastName, username: userData.username, ...values, }; @@ -226,7 +226,7 @@ const UserProfile: FC = () => { async function deleteUser() { try { await deleteUserMutation({ - params: { path: { id: user.sub } }, + params: { path: { id: user && user.sub } }, }); messageApi.success({ content: 'Your account was deleted' }); logout(); @@ -281,12 +281,12 @@ const UserProfile: FC = () => { { label: 'First Name', submitField: 'firstName', - userDataField: 'given_name', + userDataField: 'firstName', }, { label: 'Last Name', submitField: 'lastName', - userDataField: 'family_name', + userDataField: 'lastName', }, { label: 'Username', @@ -306,8 +306,8 @@ const UserProfile: FC = () => { setChangeNameModalOpen(true)} /> diff --git a/src/management-system-v2/lib/openapiSchema.ts b/src/management-system-v2/lib/openapiSchema.ts index 8691e9947..be036fa07 100644 --- a/src/management-system-v2/lib/openapiSchema.ts +++ b/src/management-system-v2/lib/openapiSchema.ts @@ -332,7 +332,7 @@ export interface components { created_at?: string; email_verified?: boolean; updated_at?: string; - user_id?: string; + id?: string; }; /** user */ userDataPut: { @@ -350,8 +350,8 @@ export interface components { /** Format: uri */ picture?: string; username?: string; - family_name?: string; - given_name?: string; + lastName?: string; + firstName?: string; }; userResponse: WithRequired< components['schemas']['userMetaData'] & components['schemas']['userData'], @@ -362,9 +362,9 @@ export interface components { | 'picture' | 'updated_at' | 'username' - | 'family_name' - | 'given_name' - | 'user_id' + | 'lastName' + | 'firstName' + | 'id' >; /** * PermissionNumber @@ -998,13 +998,7 @@ export interface operations { /** @description OK */ 200: { content: { - 'application/json': components['schemas']['userResponse']; - }; - }; - /** @description No Content */ - 204: { - content: { - 'application/json': unknown[]; + 'application/json': components['schemas']['userResponse'][]; }; }; /** @description Bad Request */ @@ -1015,12 +1009,16 @@ export interface operations { }; /** @description Create a user. */ postUser: { - requestBody?: { + requestBody: { content: { - 'application/json': WithRequired< - components['schemas']['userData'], - 'email' | 'name' | 'picture' | 'username' | 'family_name' | 'given_name' - >; + 'application/json': { + /** Format: email */ + email: string; + username: string; + lastName?: string; + firstName: string; + password: string; + }; }; }; responses: { diff --git a/src/management-system/src/backend/openapi.json b/src/management-system/src/backend/openapi.json index 8fc39cb83..5f191c0cf 100644 --- a/src/management-system/src/backend/openapi.json +++ b/src/management-system/src/backend/openapi.json @@ -907,23 +907,13 @@ "responses": { "200": { "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/userResponse" - } - } - } - }, - "204": { - "description": "No Content", "content": { "application/json": { "schema": { "type": "array", - "maxItems": 0, - "minItems": 0, - "items": {} + "items": { + "$ref": "#/components/schemas/userResponse" + } } } } @@ -956,15 +946,30 @@ } }, "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/userData" + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "username": { + "type": "string" + }, + "lastName ": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "password": { + "type": "string" } - ], - "required": ["email", "name", "picture", "username", "family_name", "given_name"] + }, + "required": ["email", "username", "lastName", "firstName", "password"] } } } @@ -2040,7 +2045,7 @@ "updated_at": { "type": "string" }, - "user_id": { + "id": { "type": "string" } } @@ -2083,10 +2088,10 @@ "username": { "type": "string" }, - "family_name": { + "lastName ": { "type": "string" }, - "given_name": { + "firstName": { "type": "string" } } @@ -2108,9 +2113,9 @@ "picture", "updated_at", "username", - "family_name", - "given_name", - "user_id" + "lastName", + "firstName", + "id" ] }, "PermissionNumber": { diff --git a/src/management-system/src/backend/server/iam/rest-api/auth0/user.js b/src/management-system/src/backend/server/iam/rest-api/auth0/user.js index 21c981d99..4c240bbf1 100644 --- a/src/management-system/src/backend/server/iam/rest-api/auth0/user.js +++ b/src/management-system/src/backend/server/iam/rest-api/auth0/user.js @@ -48,9 +48,7 @@ userRouter.get('/', isAuthenticated(), async (req, res) => { undefined, config, ); - if (users.length === 0) { - return res.status(204).json([]); - } + return res.status(200).json( users.map( ({ @@ -109,19 +107,21 @@ userRouter.get('/:id', isAuthenticated(), async (req, res) => { username, blocked, } = await requestResource(`/users/${id}`, undefined, config); - return res.status(200).json({ - created_at, - email, - email_verified, - family_name, - given_name, - name, - picture, - updated_at, - user_id, - username, - blocked, - }); + return res.status(200).json( + standardizeUser({ + created_at, + email, + email_verified, + family_name, + given_name, + name, + picture, + updated_at, + user_id, + username, + blocked, + }), + ); } catch (e) { return res.status(400).json({ error: e.toString() }); } From cd30d33457f5b61b5c4908ce99ca63090bfe97cd Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 29 Sep 2023 16:00:16 +0200 Subject: [PATCH 02/44] Data hooks: added MutationOptions and changed keys --- src/management-system-v2/lib/fetch-data.ts | 57 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/management-system-v2/lib/fetch-data.ts b/src/management-system-v2/lib/fetch-data.ts index 2c527bb5f..9272b24a6 100644 --- a/src/management-system-v2/lib/fetch-data.ts +++ b/src/management-system-v2/lib/fetch-data.ts @@ -1,5 +1,11 @@ import { useAuthStore } from './iam'; -import { UseQueryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + UseMutationOptions, + UseQueryOptions, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import createClient from 'openapi-fetch'; import { paths } from './openapiSchema'; import { useMemo } from 'react'; @@ -106,21 +112,37 @@ export function useGetAsset< export function usePostAsset[0]>( path: TFirstParam, + mutationParams: Omit = {}, ) { + type FunctionType = typeof apiClient.post; + type Data = QueryData; + const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (body: Parameters>[1]) => { + mutationFn: async (body: Parameters[1]) => { const { data } = await post(path, body); - queryClient.invalidateQueries([path, (body as any).params.path]); + const keys: any[] = [path]; + if ( + typeof body === 'object' && + 'params' in body && + typeof body.params === 'object' && + 'path' in body.params + ) { + keys.push(body.params.path); + } + + queryClient.invalidateQueries(keys); - return data as QueryData>; + return data as Data; }, + ...(mutationParams as Omit, 'mutationFn'>), }); } export function usePutAsset[0]>( path: TFirstParam, + mutationParams: Omit = {}, ) { const queryClient = useQueryClient(); return useMutation({ @@ -131,23 +153,48 @@ export function usePutAsset const { data } = await put(path, body); - queryClient.invalidateQueries([path, (body as any).params.path]); + const keys: any[] = [path]; + if ( + typeof body === 'object' && + 'params' in body && + typeof body.params === 'object' && + 'path' in body.params + ) { + keys.push(body.params.path); + } + + queryClient.invalidateQueries(keys); return data as QueryData>; }, + ...mutationParams, }); } export const useDeleteAsset = [0]>( path: TFirstParam, + mutationParams: Omit = {}, ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body: Parameters>[1]) => { const { data } = await del(path, body); + const keys: any[] = [path]; + if ( + typeof body === 'object' && + 'params' in body && + typeof body.params === 'object' && + 'path' in body.params + ) { + keys.push(body.params.path); + } + + queryClient.invalidateQueries(keys); + return data as QueryData>; }, + ...mutationParams, }); }; From 63f5c6116847a96a2bf082f52324465160084dc8 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Fri, 29 Sep 2023 16:11:07 +0200 Subject: [PATCH 03/44] Added Users page and Comming Soon page for roles --- .../app/(dashboard)/iam/roles/page.tsx | 3 + .../(dashboard)/iam/users/header-actions.tsx | 196 ++++++++++++++++++ .../app/(dashboard)/iam/users/page.tsx | 193 +++++++++++++++++ .../app/(dashboard)/layout.tsx | 29 +++ 4 files changed, 421 insertions(+) create mode 100644 src/management-system-v2/app/(dashboard)/iam/roles/page.tsx create mode 100644 src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx create mode 100644 src/management-system-v2/app/(dashboard)/iam/users/page.tsx diff --git a/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx new file mode 100644 index 000000000..5c15ae764 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/iam/roles/page.tsx @@ -0,0 +1,3 @@ +export default function RolesPage() { + return

Comming soon

; +} 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 new file mode 100644 index 000000000..f8ce62994 --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/iam/users/header-actions.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { ApiRequestBody, usePostAsset } from '@/lib/fetch-data'; +import { AuthCan } from '@/lib/iamComponents'; +import { ImportOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, App, Input, Modal } from 'antd'; +import { ComponentProps, FC, ReactNode, useEffect, useState } from 'react'; + +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<{ + 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 { 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(

{error}

); + } else { + errors[key] = [

{error}

]; + } + } + + 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); + }, + }); + + useEffect(() => { + form.resetFields(); + setFormatError({}); + }, [form, modalOpen]); + + const submitData = async ( + values: Record | 'confirm_password', string>, + ) => { + debugger; + 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' }); + } + }; + + return ( + +
+ {modalStructureWithoutPassword.map((formField) => ( + + + + ))} + + + + + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('The new password that you entered do not match!')); + }, + }), + ]} + > + + + + + + +
+
+ ); +}; + +const HeaderActions: FC = () => { + const [createUserModalOpen, setCreateUserModalOpen] = useState(false); + + return ( + <> + setCreateUserModalOpen(false)} + /> + + + + + + ); +}; + +export default HeaderActions; diff --git a/src/management-system-v2/app/(dashboard)/iam/users/page.tsx b/src/management-system-v2/app/(dashboard)/iam/users/page.tsx new file mode 100644 index 000000000..29a368e8b --- /dev/null +++ b/src/management-system-v2/app/(dashboard)/iam/users/page.tsx @@ -0,0 +1,193 @@ +'use client'; + +import React, { FC, useMemo, useState } from 'react'; +import styles from '@/components/processes.module.scss'; +import cn from 'classnames'; +import { DeleteOutlined } from '@ant-design/icons'; +import Fuse from 'fuse.js'; +import { + Tooltip, + Space, + Avatar, + Row, + Col, + Button, + Input, + Result, + Table, + Popconfirm, + App, +} from 'antd'; +import { ApiData, useGetAsset, useDeleteAsset } from '@/lib/fetch-data'; +import { CloseOutlined } from '@ant-design/icons'; +import Auth from '@/lib/AuthCanWrapper'; +import Content from '@/components/content'; +import HeaderActions from './header-actions'; + +type User = ApiData<'/users/{id}', 'get'>; + +const UsersPage: FC = () => { + const { error, data, isLoading, refetch: refetchUsers } = useGetAsset('/users', {}); + const { message: messageApi } = App.useApp(); + const { mutateAsync: deleteUser, isLoading: deletingUser } = useDeleteAsset('/users/{id}', { + onSuccess: () => refetchUsers(), + onError: () => messageApi.open({ type: 'error', content: 'Something went wrong' }), + }); + + const users = useMemo(() => { + if (!data) return []; + + return data.map((user) => ({ + ...user, + display: ( + + + + {user.firstName} {user.lastName} + + + ), + })); + }, [data]); + + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + + const fuse = useMemo( + () => + users && + new Fuse(users, { + findAllMatches: true, + threshold: 0.75, + useExtendedSearch: true, + ignoreLocation: true, + keys: ['firstName', 'lastName', 'username', 'email'] as (keyof User)[], + }), + [users], + ); + + const filteredUsers = useMemo(() => { + if (!fuse || searchQuery === '') return users; + + const filt = fuse.search(searchQuery).map((result) => result.item); + return filt; + }, [fuse, searchQuery, users]); + + async function deleteUsers(userIds: string[]) { + setSelectedRowKeys([]); + await Promise.allSettled(userIds.map((id) => deleteUser({ params: { path: { id } } }))); + } + + const columns = [ + { + title: 'Account', + dataIndex: 'display', + key: 'display', + }, + { + title: 'Username', + dataIndex: 'username', + key: 'username', + }, + { + title: 'Email Adress', + dataIndex: 'email', + key: 'email', + }, + { + dataIndex: 'id', + key: 'tooltip', + title: '', + with: 100, + render: (id: string) => + selectedRowKeys.length === 0 ? ( + + deleteUsers([id])} + > +