Skip to content

Commit

Permalink
Nextauth ms2 (#127)
Browse files Browse the repository at this point in the history
* Installed server-only and next-auth

* MS: endpoint for fetching ability rules

* MS2: implemented nextauth and refacored auth code

* Use nextauth cookies for auth in old ms

* Example .env.local file

* Changed page to be rendered on the server

* Double submit cookie csrf protection

* Local development users

* Transformed page to be a server component

* use environments for auth and auth0 is optional in dev

* Enabled redirects in AuthCan

* Added missing commands

* Feature flag for auth0 + ms api script

* Fallback user avatar

* Updated readme

* Fixed typos
  • Loading branch information
FelipeTrost authored Nov 12, 2023
1 parent 54bc2f9 commit 5c3a8c0
Show file tree
Hide file tree
Showing 49 changed files with 1,268 additions and 953 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,18 @@ _Server version:_ If successful, this automatically starts a Chrome/Chromium bro
**Authentication & Authorization**

The API (`yarn dev-ms`) creates two users when it starts:
Wether you start the API with `yarn dev-ms-api` or `yarn dev-ms-api-auth0` you can log in with two default users just by typing their name in the 'Sign in with Development Users' section:

- Admin: With the username `admin`.
- John Doe: With the username `johndoe`.

If you start the API with `yarn dev-ms-api-auth0`, two users are created on the development Auth0 environment:

- Admin: With the username `admin` and the password `ProceedAdm1n!`.
- John Doe: With the username `johndoe` and the password `JohnDoe1!`.

> :warning: To use `yarn dev-ms-api-auth0` you need access to the private environments repository.
## Testing

Before committing a new version, a linting check is automatically done.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dev-ms": "cd src/management-system-v2 && cross-env USE_AUTHORIZATION=true yarn dev",
"dev-ms-api-no-iam": "cd src/management-system && cross-env process.env.API_ONLY=true USE_AUTHORIZATION=false yarn web:dev-start-backend",
"dev-ms-api": "cd src/management-system && cross-env process.env.API_ONLY=true yarn web:dev-iam",
"dev-ms-api-auth0": "cd src/management-system && cross-env process.env.API_ONLY=true process.env.USE_AUTH0=true yarn web:dev-iam",
"dev-ms-old": "cd src/management-system && yarn web:dev",
"dev-ms-old-iam": "cd src/management-system && yarn web:dev-iam",
"dev-web": "yarn build && cd src/engine/native/web/server && yarn serve",
Expand Down
7 changes: 7 additions & 0 deletions src/management-system-v2/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NEXTAUTH_URL=
NEXTAUTH_SECRET=

AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
AUTH0_ISSUER=
AUTH0_SCOPE=
Original file line number Diff line number Diff line change
@@ -1,68 +1,5 @@
'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>
);
}
import Auth from '@/lib/serverAuthComponents';
import RolePage from './role-page';

export default Auth(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import Content from '@/components/content';
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 RolePage;
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Auth from '@/lib/AuthCanWrapper';
'use client';

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';
import { useAbilityStore } from '@/lib/abilityStore';

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

const { data, isLoading, error } = useGetAsset('/roles/{id}', {
Expand Down Expand Up @@ -80,11 +81,4 @@ const RoleGeneralData: FC<{ roleId: string }> = ({ roleId }) => {
);
};

export default Auth(
{
action: ['view', 'manage'],
resource: 'Role',
fallbackRedirect: '/',
},
RoleGeneralData,
);
export default RoleGeneralData;
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { SaveOutlined, LoadingOutlined } from '@ant-design/icons';
import { ResourceActionType } from '@/lib/ability/caslAbility';
import { FC, useState } from 'react';
import { ApiData, usePutAsset } from '@/lib/fetch-data';
import { useAuthStore } from '@/lib/iam';
import { switchChecked, switchDisabled, togglePermission } from './role-permissions-helper';
import { useAbilityStore } from '@/lib/abilityStore';

type PermissionCategory = {
key: string;
Expand Down Expand Up @@ -260,7 +260,7 @@ type Role = ApiData<'/roles', 'get'>[number];

const RolePermissions: FC<{ role: Role }> = ({ role }) => {
const [permissions, setPermissions] = useState(role.permissions);
const ability = useAuthStore((store) => store.ability);
const ability = useAbilityStore((store) => store.ability);
const { mutateAsync, isLoading } = usePutAsset('/roles/{id}', {
/* onSuccess: () => message.open({ content: 'Role updated', type: 'success' }),
onError: () => message.open({ content: 'Something went wrong', type: 'error' }), */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';

import { AuthCan } from '@/lib/clientAuthComponents';
import { ApiRequestBody, usePostAsset } from '@/lib/fetch-data';
import { AuthCan } from '@/lib/iamComponents';
import { ImportOutlined, PlusOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, App, Input, Modal } from 'antd';
import { ComponentProps, FC, ReactNode, useEffect, useState } from 'react';
import { FC, ReactNode, useEffect, useState } from 'react';

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

Expand Down
146 changes: 2 additions & 144 deletions src/management-system-v2/app/(dashboard)/iam/roles/page.tsx
Original file line number Diff line number Diff line change
@@ -1,147 +1,5 @@
'use client';

import React, { FC, useState } from 'react';
import styles from '@/components/processes.module.scss';
import cn from 'classnames';
import { DeleteOutlined } from '@ant-design/icons';
import { Tooltip, Space, Row, Col, Button, Input, Result, Table, Popconfirm, App } from 'antd';
import { useGetAsset, useDeleteAsset, ApiData } 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';
import useFuzySearch from '@/lib/useFuzySearch';
import { AuthCan } from '@/lib/iamComponents';
import Link from 'next/link';
import { toCaslResource } from '@/lib/ability/caslAbility';
import Bar from '@/components/bar';

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

const RolesPage: FC = () => {
const { message: messageApi } = App.useApp();

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'], {
useSearchParams: false,
});

const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);

async function deleteRoles(userIds: string[]) {
setSelectedRowKeys([]);
await Promise.allSettled(userIds.map((id) => deleteRole({ params: { path: { id } } })));
}

const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'display',
render: (name: string, role: Role) => <Link href={`/iam/roles/${role.id}`}>{name}</Link>,
},
{
title: 'Members',
dataIndex: 'members',
render: (_: any, record: Role) => record.members.length,
key: 'username',
},
{
dataIndex: 'id',
key: 'tooltip',
title: '',
with: 100,
render: (id: string, role: Role) =>
selectedRowKeys.length === 0 ? (
<AuthCan action="delete" resource={toCaslResource('Role', role)}>
<Tooltip placement="top" title={'Delete'}>
<Popconfirm
title="Delete User"
description="Are you sure you want to delete this user?"
onConfirm={() => deleteRoles([id])}
>
<Button icon={<DeleteOutlined />} type="text" />
</Popconfirm>
</Tooltip>
</AuthCan>
) : null,
},
];

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

return (
<Content title="Identity and Access Management">
<Bar rightNode={<HeaderActions />} />
<Row className={styles.Headerrow}>
<Col
xs={24}
sm={24}
md={24}
lg={10}
xl={6}
className={cn({ [styles.SelectedRow]: selectedRowKeys.length > 0 })}
style={{
justifyContent: 'start',
}}
>
{selectedRowKeys.length > 0 ? (
<Space size={20}>
<Button type="text" icon={<CloseOutlined />} onClick={() => setSelectedRowKeys([])} />
<span>{selectedRowKeys.length} selected: </span>
<Popconfirm
title="Delete User"
description="Are you sure you want to delete this user?"
onConfirm={() => deleteRoles(selectedRowKeys)}
>
<Button type="text" icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
) : null}
</Col>
<Col className={styles.Headercol} xs={22} sm={22} md={22} lg={9} xl={13}>
<Input.Search
size="middle"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
allowClear
placeholder="Search Users"
/>
</Col>
</Row>

<Table
columns={columns}
dataSource={filteredRoles}
rowSelection={{
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys as string[]);
},
}}
rowKey="id"
loading={isLoading || deletingRole}
size="middle"
/>
</Content>
);
};
import Auth from '@/lib/serverAuthComponents';
import RolesPage from './role-page';

export default Auth(
{
Expand Down
Loading

0 comments on commit 5c3a8c0

Please sign in to comment.