From 9a0fee65a51c63837f3344a6923d1e04aa04033d Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Fri, 23 May 2025 00:28:29 +0700 Subject: [PATCH] fixing so much trouble --- .../_components/modal-edit-account.tsx | 0 .../app/{ => (protected)}/accounts/page.tsx | 4 +- .../dashboard/_components/modal-add-item.tsx | 0 .../_components/modal-delete-item.tsx | 0 .../dashboard/_components/modal-edit-item.tsx | 0 .../dashboard/_hook/use-item.ts | 0 .../app/{ => (protected)}/dashboard/page.tsx | 2 +- .../gacha-roll/_components/modal-add-item.tsx | 0 .../_components/modal-delete-item.tsx | 0 .../_components/modal-update-item.tsx | 0 .../gacha-roll/_hook/use-item.ts | 0 .../app/{ => (protected)}/gacha-roll/page.tsx | 0 .../app/{accounts => (protected)}/layout.tsx | 0 .../_components/modal-add-permission.tsx | 0 .../_components/modal-delete-permission.tsx | 0 .../_components/modal-update-permission.tsx | 0 .../permissions/_hook/use-item.ts | 0 .../{ => (protected)}/permissions/page.tsx | 0 .../prizes/_components/modal-process-item.tsx | 0 .../src/app/{ => (protected)}/prizes/page.tsx | 0 .../roles/_components/modal-add-role.tsx | 0 .../roles/_components/modal-delete-role.tsx | 0 .../roles/_components/modal-update-role.tsx | 0 .../{ => (protected)}/roles/_hook/use-item.ts | 0 .../src/app/{ => (protected)}/roles/page.tsx | 0 .../_components/modal-validate.tsx | 0 .../{ => (protected)}/transactions/page.tsx | 0 .../auth/login}/_hooks/use-login.ts | 13 +- .../app/{ => (public)/auth/login}/page.tsx | 11 +- .../src/app/{ => (public)}/layout.tsx | 0 apps/backoffice/src/app/dashboard/layout.tsx | 18 -- apps/backoffice/src/app/gacha-roll/layout.tsx | 18 -- .../backoffice/src/app/permissions/layout.tsx | 18 -- apps/backoffice/src/app/prizes/layout.tsx | 18 -- apps/backoffice/src/app/roles/layout.tsx | 18 -- .../src/app/transactions/layout.tsx | 18 -- apps/backoffice/src/middleware.ts | 103 +++++++- apps/dimentorin/src/app/_hooks/use-login.ts | 13 +- apps/dimentorin/src/middleware.ts | 104 +++++++- apps/gacha/src/app/_hooks/use-login.ts | 11 +- apps/gacha/src/app/_hooks/use-register.ts | 25 +- apps/gacha/src/middleware.ts | 104 +++++++- libs/service/src/hooks/auth/index.ts | 6 - libs/service/src/schemas/auth/index.ts | 33 +-- libs/service/src/types/auth/index.ts | 37 ++- libs/service/src/types/common/index.ts | 14 +- libs/service/src/types/roles/index.ts | 12 +- libs/service/src/types/users/index.ts | 10 +- libs/utils/src/constants/index.ts | 1 + libs/utils/src/constants/permissions.ts | 40 ++++ libs/utils/src/cookies/index.ts | 36 +++ libs/utils/src/hooks/index.ts | 3 +- libs/utils/src/hooks/use-auth-store.ts | 47 ++++ libs/utils/src/hooks/use-session.ts | 39 ++- libs/utils/src/index.ts | 3 + libs/utils/src/local-storage/index.ts | 27 +-- .../src/react-router/file-based-routing.tsx | 222 +++++++++++------- libs/utils/src/session/guard/index.tsx | 27 +++ libs/utils/src/session/index.ts | 1 + package-lock.json | 32 ++- package.json | 9 +- 61 files changed, 735 insertions(+), 362 deletions(-) rename apps/backoffice/src/app/{ => (protected)}/accounts/_components/modal-edit-account.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/accounts/page.tsx (97%) rename apps/backoffice/src/app/{ => (protected)}/dashboard/_components/modal-add-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/dashboard/_components/modal-delete-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/dashboard/_components/modal-edit-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/dashboard/_hook/use-item.ts (100%) rename apps/backoffice/src/app/{ => (protected)}/dashboard/page.tsx (99%) rename apps/backoffice/src/app/{ => (protected)}/gacha-roll/_components/modal-add-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/gacha-roll/_components/modal-delete-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/gacha-roll/_components/modal-update-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/gacha-roll/_hook/use-item.ts (100%) rename apps/backoffice/src/app/{ => (protected)}/gacha-roll/page.tsx (100%) rename apps/backoffice/src/app/{accounts => (protected)}/layout.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/permissions/_components/modal-add-permission.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/permissions/_components/modal-delete-permission.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/permissions/_components/modal-update-permission.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/permissions/_hook/use-item.ts (100%) rename apps/backoffice/src/app/{ => (protected)}/permissions/page.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/prizes/_components/modal-process-item.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/prizes/page.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/roles/_components/modal-add-role.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/roles/_components/modal-delete-role.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/roles/_components/modal-update-role.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/roles/_hook/use-item.ts (100%) rename apps/backoffice/src/app/{ => (protected)}/roles/page.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/transactions/_components/modal-validate.tsx (100%) rename apps/backoffice/src/app/{ => (protected)}/transactions/page.tsx (100%) rename apps/backoffice/src/app/{ => (public)/auth/login}/_hooks/use-login.ts (56%) rename apps/backoffice/src/app/{ => (public)/auth/login}/page.tsx (83%) rename apps/backoffice/src/app/{ => (public)}/layout.tsx (100%) delete mode 100644 apps/backoffice/src/app/dashboard/layout.tsx delete mode 100644 apps/backoffice/src/app/gacha-roll/layout.tsx delete mode 100644 apps/backoffice/src/app/permissions/layout.tsx delete mode 100644 apps/backoffice/src/app/prizes/layout.tsx delete mode 100644 apps/backoffice/src/app/roles/layout.tsx delete mode 100644 apps/backoffice/src/app/transactions/layout.tsx create mode 100644 libs/utils/src/constants/index.ts create mode 100644 libs/utils/src/constants/permissions.ts create mode 100644 libs/utils/src/cookies/index.ts create mode 100644 libs/utils/src/hooks/use-auth-store.ts create mode 100644 libs/utils/src/session/guard/index.tsx create mode 100644 libs/utils/src/session/index.ts diff --git a/apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx b/apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx similarity index 100% rename from apps/backoffice/src/app/accounts/_components/modal-edit-account.tsx rename to apps/backoffice/src/app/(protected)/accounts/_components/modal-edit-account.tsx diff --git a/apps/backoffice/src/app/accounts/page.tsx b/apps/backoffice/src/app/(protected)/accounts/page.tsx similarity index 97% rename from apps/backoffice/src/app/accounts/page.tsx rename to apps/backoffice/src/app/(protected)/accounts/page.tsx index e762610..dd246b4 100644 --- a/apps/backoffice/src/app/accounts/page.tsx +++ b/apps/backoffice/src/app/(protected)/accounts/page.tsx @@ -18,7 +18,7 @@ import { RowSelectionState, } from '@tanstack/react-table'; import ModalEditAccount from './_components/modal-edit-account'; -import { useQueryState } from '../../hook/use-query-state'; +import { useQueryState } from '@imphnen-frontend-service/utils'; interface Account { id: number; @@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => { {showFilter && (
- setShowFilter(false)} /> + setShowFilter(false)} options={[]} />
)} diff --git a/apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx similarity index 100% rename from apps/backoffice/src/app/dashboard/_components/modal-add-item.tsx rename to apps/backoffice/src/app/(protected)/dashboard/_components/modal-add-item.tsx diff --git a/apps/backoffice/src/app/dashboard/_components/modal-delete-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx similarity index 100% rename from apps/backoffice/src/app/dashboard/_components/modal-delete-item.tsx rename to apps/backoffice/src/app/(protected)/dashboard/_components/modal-delete-item.tsx diff --git a/apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx b/apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx similarity index 100% rename from apps/backoffice/src/app/dashboard/_components/modal-edit-item.tsx rename to apps/backoffice/src/app/(protected)/dashboard/_components/modal-edit-item.tsx diff --git a/apps/backoffice/src/app/dashboard/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts similarity index 100% rename from apps/backoffice/src/app/dashboard/_hook/use-item.ts rename to apps/backoffice/src/app/(protected)/dashboard/_hook/use-item.ts diff --git a/apps/backoffice/src/app/dashboard/page.tsx b/apps/backoffice/src/app/(protected)/dashboard/page.tsx similarity index 99% rename from apps/backoffice/src/app/dashboard/page.tsx rename to apps/backoffice/src/app/(protected)/dashboard/page.tsx index be30690..f456c48 100644 --- a/apps/backoffice/src/app/dashboard/page.tsx +++ b/apps/backoffice/src/app/(protected)/dashboard/page.tsx @@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react'; import ModalAddItem from './_components/modal-add-item'; import ModalEditItem from './_components/modal-edit-item'; import ModalDeleteItem from './_components/modal-delete-item'; -import { useQueryState } from '../../hook/use-query-state'; +import { useQueryState } from '@imphnen-frontend-service/utils'; export const Components: FC = (): ReactElement => { const [showModalAddItem, setShowModalAddItem] = useState(false); diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx similarity index 100% rename from apps/backoffice/src/app/gacha-roll/_components/modal-add-item.tsx rename to apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-add-item.tsx diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-delete-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx similarity index 100% rename from apps/backoffice/src/app/gacha-roll/_components/modal-delete-item.tsx rename to apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-delete-item.tsx diff --git a/apps/backoffice/src/app/gacha-roll/_components/modal-update-item.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx similarity index 100% rename from apps/backoffice/src/app/gacha-roll/_components/modal-update-item.tsx rename to apps/backoffice/src/app/(protected)/gacha-roll/_components/modal-update-item.tsx diff --git a/apps/backoffice/src/app/gacha-roll/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts similarity index 100% rename from apps/backoffice/src/app/gacha-roll/_hook/use-item.ts rename to apps/backoffice/src/app/(protected)/gacha-roll/_hook/use-item.ts diff --git a/apps/backoffice/src/app/gacha-roll/page.tsx b/apps/backoffice/src/app/(protected)/gacha-roll/page.tsx similarity index 100% rename from apps/backoffice/src/app/gacha-roll/page.tsx rename to apps/backoffice/src/app/(protected)/gacha-roll/page.tsx diff --git a/apps/backoffice/src/app/accounts/layout.tsx b/apps/backoffice/src/app/(protected)/layout.tsx similarity index 100% rename from apps/backoffice/src/app/accounts/layout.tsx rename to apps/backoffice/src/app/(protected)/layout.tsx diff --git a/apps/backoffice/src/app/permissions/_components/modal-add-permission.tsx b/apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx similarity index 100% rename from apps/backoffice/src/app/permissions/_components/modal-add-permission.tsx rename to apps/backoffice/src/app/(protected)/permissions/_components/modal-add-permission.tsx diff --git a/apps/backoffice/src/app/permissions/_components/modal-delete-permission.tsx b/apps/backoffice/src/app/(protected)/permissions/_components/modal-delete-permission.tsx similarity index 100% rename from apps/backoffice/src/app/permissions/_components/modal-delete-permission.tsx rename to apps/backoffice/src/app/(protected)/permissions/_components/modal-delete-permission.tsx diff --git a/apps/backoffice/src/app/permissions/_components/modal-update-permission.tsx b/apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx similarity index 100% rename from apps/backoffice/src/app/permissions/_components/modal-update-permission.tsx rename to apps/backoffice/src/app/(protected)/permissions/_components/modal-update-permission.tsx diff --git a/apps/backoffice/src/app/permissions/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts similarity index 100% rename from apps/backoffice/src/app/permissions/_hook/use-item.ts rename to apps/backoffice/src/app/(protected)/permissions/_hook/use-item.ts diff --git a/apps/backoffice/src/app/permissions/page.tsx b/apps/backoffice/src/app/(protected)/permissions/page.tsx similarity index 100% rename from apps/backoffice/src/app/permissions/page.tsx rename to apps/backoffice/src/app/(protected)/permissions/page.tsx diff --git a/apps/backoffice/src/app/prizes/_components/modal-process-item.tsx b/apps/backoffice/src/app/(protected)/prizes/_components/modal-process-item.tsx similarity index 100% rename from apps/backoffice/src/app/prizes/_components/modal-process-item.tsx rename to apps/backoffice/src/app/(protected)/prizes/_components/modal-process-item.tsx diff --git a/apps/backoffice/src/app/prizes/page.tsx b/apps/backoffice/src/app/(protected)/prizes/page.tsx similarity index 100% rename from apps/backoffice/src/app/prizes/page.tsx rename to apps/backoffice/src/app/(protected)/prizes/page.tsx diff --git a/apps/backoffice/src/app/roles/_components/modal-add-role.tsx b/apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx similarity index 100% rename from apps/backoffice/src/app/roles/_components/modal-add-role.tsx rename to apps/backoffice/src/app/(protected)/roles/_components/modal-add-role.tsx diff --git a/apps/backoffice/src/app/roles/_components/modal-delete-role.tsx b/apps/backoffice/src/app/(protected)/roles/_components/modal-delete-role.tsx similarity index 100% rename from apps/backoffice/src/app/roles/_components/modal-delete-role.tsx rename to apps/backoffice/src/app/(protected)/roles/_components/modal-delete-role.tsx diff --git a/apps/backoffice/src/app/roles/_components/modal-update-role.tsx b/apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx similarity index 100% rename from apps/backoffice/src/app/roles/_components/modal-update-role.tsx rename to apps/backoffice/src/app/(protected)/roles/_components/modal-update-role.tsx diff --git a/apps/backoffice/src/app/roles/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts similarity index 100% rename from apps/backoffice/src/app/roles/_hook/use-item.ts rename to apps/backoffice/src/app/(protected)/roles/_hook/use-item.ts diff --git a/apps/backoffice/src/app/roles/page.tsx b/apps/backoffice/src/app/(protected)/roles/page.tsx similarity index 100% rename from apps/backoffice/src/app/roles/page.tsx rename to apps/backoffice/src/app/(protected)/roles/page.tsx diff --git a/apps/backoffice/src/app/transactions/_components/modal-validate.tsx b/apps/backoffice/src/app/(protected)/transactions/_components/modal-validate.tsx similarity index 100% rename from apps/backoffice/src/app/transactions/_components/modal-validate.tsx rename to apps/backoffice/src/app/(protected)/transactions/_components/modal-validate.tsx diff --git a/apps/backoffice/src/app/transactions/page.tsx b/apps/backoffice/src/app/(protected)/transactions/page.tsx similarity index 100% rename from apps/backoffice/src/app/transactions/page.tsx rename to apps/backoffice/src/app/(protected)/transactions/page.tsx diff --git a/apps/backoffice/src/app/_hooks/use-login.ts b/apps/backoffice/src/app/(public)/auth/login/_hooks/use-login.ts similarity index 56% rename from apps/backoffice/src/app/_hooks/use-login.ts rename to apps/backoffice/src/app/(public)/auth/login/_hooks/use-login.ts index 3c031d3..e0323ce 100644 --- a/apps/backoffice/src/app/_hooks/use-login.ts +++ b/apps/backoffice/src/app/(public)/auth/login/_hooks/use-login.ts @@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form'; import { authLoginSchema, TLoginRequest, - usePostLogin, } from '@imphnen-frontend-service/service'; import { zodResolver } from '@hookform/resolvers/zod'; -import { toast } from 'sonner'; +import { useSession } from '@imphnen-frontend-service/utils'; export const useLogin = () => { - const postLogin = usePostLogin(); const form = useForm({ resolver: zodResolver(authLoginSchema), mode: 'all', }); - const onSubmit = form.handleSubmit((data) => { - postLogin.mutate(data, { - onSuccess: () => toast.success("Login sukses"), - onError: (error) => toast.error(error.message), - }); - }); + const { signIn } = useSession(); + + const onSubmit = form.handleSubmit((data) => signIn(data)); return { form, diff --git a/apps/backoffice/src/app/page.tsx b/apps/backoffice/src/app/(public)/auth/login/page.tsx similarity index 83% rename from apps/backoffice/src/app/page.tsx rename to apps/backoffice/src/app/(public)/auth/login/page.tsx index 99b0c4e..bb0a020 100644 --- a/apps/backoffice/src/app/page.tsx +++ b/apps/backoffice/src/app/(public)/auth/login/page.tsx @@ -32,7 +32,16 @@ export const Components: FC = (): ReactElement => { size="lg" className="w-full" /> - diff --git a/apps/backoffice/src/app/layout.tsx b/apps/backoffice/src/app/(public)/layout.tsx similarity index 100% rename from apps/backoffice/src/app/layout.tsx rename to apps/backoffice/src/app/(public)/layout.tsx diff --git a/apps/backoffice/src/app/dashboard/layout.tsx b/apps/backoffice/src/app/dashboard/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/dashboard/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/app/gacha-roll/layout.tsx b/apps/backoffice/src/app/gacha-roll/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/gacha-roll/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/app/permissions/layout.tsx b/apps/backoffice/src/app/permissions/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/permissions/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/app/prizes/layout.tsx b/apps/backoffice/src/app/prizes/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/prizes/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/app/roles/layout.tsx b/apps/backoffice/src/app/roles/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/roles/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/app/transactions/layout.tsx b/apps/backoffice/src/app/transactions/layout.tsx deleted file mode 100644 index a770c99..0000000 --- a/apps/backoffice/src/app/transactions/layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FC, ReactElement } from 'react'; -import { Outlet } from 'react-router-dom'; -import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms'; - -export const AppLayout: FC = (): ReactElement => { - return ( -
-
- -
- -
-
-
- ); -}; - -export default AppLayout; diff --git a/apps/backoffice/src/middleware.ts b/apps/backoffice/src/middleware.ts index f32b107..f47b40b 100644 --- a/apps/backoffice/src/middleware.ts +++ b/apps/backoffice/src/middleware.ts @@ -1,11 +1,108 @@ -import { LoaderFunctionArgs } from 'react-router-dom'; +import { + PERMISSIONS, + SessionToken, + SessionUser, +} from '@imphnen-frontend-service/utils'; +import { LoaderFunctionArgs, redirect } from 'react-router'; + +const mappingPublicRoutes = [ + '/auth/login', + '/auth/forgot', + '/auth/new-password', +]; + +const mappingRoutePermissions = [ + { + path: '/dashboard', + permissions: [], + }, + { + path: '/users', + permissions: [PERMISSIONS.USERS.READ_LIST], + }, + { + path: '/users/create', + permissions: [PERMISSIONS.USERS.CREATE], + }, + { + path: '/users/update', + permissions: [PERMISSIONS.USERS.UPDATE], + }, + { + path: '/users/detail', + permissions: [PERMISSIONS.USERS.READ_DETAIL], + }, + { + path: '/roles', + permissions: [PERMISSIONS.ROLES.READ_LIST], + }, + { + path: '/roles/create', + permissions: [PERMISSIONS.ROLES.CREATE], + }, + { + path: '/roles/update', + permissions: [PERMISSIONS.ROLES.UPDATE], + }, + { + path: '/roles/detail', + permissions: [PERMISSIONS.ROLES.READ_DETAIL], + }, + { + path: '/permissions', + permissions: [PERMISSIONS.PERMISSIONS.READ_LIST], + }, + { + path: '/permissions/create', + permissions: [PERMISSIONS.PERMISSIONS.CREATE], + }, + { + path: '/permissions/update', + permissions: [PERMISSIONS.PERMISSIONS.UPDATE], + }, + { + path: '/permissions/detail', + permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL], + }, +]; + +//TODO : Fix this later +// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => { +// const fallback = mappingRoutePermissions.find((route) => +// route.permissions.some((perm) => userPermissions.includes(perm)) +// ); +// return redirect(fallback?.path ?? '/auth/login'); +// }; export const middleware = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); - const pathname = url.pathname; + const session = SessionUser.get(); + const session_token = SessionToken.get(); + const token = session_token?.token?.access_token; + const userPermissions = + session?.role?.permissions?.map?.((perm) => perm?.name) ?? []; - if (pathname) return null; + if (mappingPublicRoutes.includes(pathname)) { + if (token) return redirect('/dashboard'); + return null; + } + + if (!session) return redirect('/auth/login'); + + const matchedRoute = mappingRoutePermissions.find( + (route) => route.path === pathname + ); + + if (matchedRoute) { + const hasPermission = + !matchedRoute.permissions || + matchedRoute.permissions.some((perm) => userPermissions.includes(perm)); + + if (!hasPermission) { + return '/dashboard'; + } + } return null; }; diff --git a/apps/dimentorin/src/app/_hooks/use-login.ts b/apps/dimentorin/src/app/_hooks/use-login.ts index 3c031d3..e0323ce 100644 --- a/apps/dimentorin/src/app/_hooks/use-login.ts +++ b/apps/dimentorin/src/app/_hooks/use-login.ts @@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form'; import { authLoginSchema, TLoginRequest, - usePostLogin, } from '@imphnen-frontend-service/service'; import { zodResolver } from '@hookform/resolvers/zod'; -import { toast } from 'sonner'; +import { useSession } from '@imphnen-frontend-service/utils'; export const useLogin = () => { - const postLogin = usePostLogin(); const form = useForm({ resolver: zodResolver(authLoginSchema), mode: 'all', }); - const onSubmit = form.handleSubmit((data) => { - postLogin.mutate(data, { - onSuccess: () => toast.success("Login sukses"), - onError: (error) => toast.error(error.message), - }); - }); + const { signIn } = useSession(); + + const onSubmit = form.handleSubmit((data) => signIn(data)); return { form, diff --git a/apps/dimentorin/src/middleware.ts b/apps/dimentorin/src/middleware.ts index f32b107..3d4d24a 100644 --- a/apps/dimentorin/src/middleware.ts +++ b/apps/dimentorin/src/middleware.ts @@ -1,11 +1,109 @@ -import { LoaderFunctionArgs } from 'react-router-dom'; +import { + PERMISSIONS, + SessionToken, + SessionUser, +} from '@imphnen-frontend-service/utils'; +import { LoaderFunctionArgs, redirect } from 'react-router'; + +const mappingPublicRoutes = [ + '/', + '/auth/login', + '/auth/forgot', + '/auth/new-password', +]; + +const mappingRoutePermissions = [ + { + path: '/dashboard', + permissions: [], + }, + { + path: '/users', + permissions: [PERMISSIONS.USERS.READ_LIST], + }, + { + path: '/users/create', + permissions: [PERMISSIONS.USERS.CREATE], + }, + { + path: '/users/update', + permissions: [PERMISSIONS.USERS.UPDATE], + }, + { + path: '/users/detail', + permissions: [PERMISSIONS.USERS.READ_DETAIL], + }, + { + path: '/roles', + permissions: [PERMISSIONS.ROLES.READ_LIST], + }, + { + path: '/roles/create', + permissions: [PERMISSIONS.ROLES.CREATE], + }, + { + path: '/roles/update', + permissions: [PERMISSIONS.ROLES.UPDATE], + }, + { + path: '/roles/detail', + permissions: [PERMISSIONS.ROLES.READ_DETAIL], + }, + { + path: '/permissions', + permissions: [PERMISSIONS.PERMISSIONS.READ_LIST], + }, + { + path: '/permissions/create', + permissions: [PERMISSIONS.PERMISSIONS.CREATE], + }, + { + path: '/permissions/update', + permissions: [PERMISSIONS.PERMISSIONS.UPDATE], + }, + { + path: '/permissions/detail', + permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL], + }, +]; + +//TODO : Fix this later +// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => { +// const fallback = mappingRoutePermissions.find((route) => +// route.permissions.some((perm) => userPermissions.includes(perm)) +// ); +// return redirect(fallback?.path ?? '/auth/login'); +// }; export const middleware = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); - const pathname = url.pathname; + const session = SessionUser.get(); + const session_token = SessionToken.get(); + const token = session_token?.token?.access_token; + const userPermissions = + session?.role?.permissions?.map?.((perm) => perm?.name) ?? []; - if (pathname) return null; + if (mappingPublicRoutes.includes(pathname)) { + if (token) return redirect('/dashboard'); + return null; + } + + if (!session) return redirect('/auth/login'); + + const matchedRoute = mappingRoutePermissions.find( + (route) => route.path === pathname + ); + + if (matchedRoute) { + const hasPermission = + !matchedRoute.permissions || + matchedRoute.permissions.some((perm) => userPermissions.includes(perm)); + + if (!hasPermission) { + return '/dashboard'; + } + } return null; }; diff --git a/apps/gacha/src/app/_hooks/use-login.ts b/apps/gacha/src/app/_hooks/use-login.ts index 272b2ba..e0323ce 100644 --- a/apps/gacha/src/app/_hooks/use-login.ts +++ b/apps/gacha/src/app/_hooks/use-login.ts @@ -2,22 +2,19 @@ import { useForm } from 'react-hook-form'; import { authLoginSchema, TLoginRequest, - usePostLogin, } from '@imphnen-frontend-service/service'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useSession } from '@imphnen-frontend-service/utils'; export const useLogin = () => { - const postLogin = usePostLogin(); const form = useForm({ resolver: zodResolver(authLoginSchema), mode: 'all', }); - const onSubmit = form.handleSubmit((data) => { - postLogin.mutate(data, { - onSuccess: () => console.log('Success Login'), - }); - }); + const { signIn } = useSession(); + + const onSubmit = form.handleSubmit((data) => signIn(data)); return { form, diff --git a/apps/gacha/src/app/_hooks/use-register.ts b/apps/gacha/src/app/_hooks/use-register.ts index 5817c30..d7c5531 100644 --- a/apps/gacha/src/app/_hooks/use-register.ts +++ b/apps/gacha/src/app/_hooks/use-register.ts @@ -1,14 +1,11 @@ import { useForm } from 'react-hook-form'; import { authRegisterSchema, - stepOneRegisterSchema, TRegisterRequest, usePostRegister, } from '@imphnen-frontend-service/service'; -import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { toast } from 'sonner'; -import { useEffect, useState } from 'react'; export const useRegister = () => { const postRegister = usePostRegister(); @@ -21,31 +18,12 @@ export const useRegister = () => { password: '', confirm_password: '', phone_number: '', - referral_code: '', - referred_by: '', - student_type: '', }, }); - const [stepValid, setStepValid] = useState(false); - - useEffect(() => { - const subscription = form.watch(() => { - const { fullname, email, password, confirm_password } = form.getValues(); - const valid = stepOneRegisterSchema.safeParse({ - fullname, - email, - password, - confirm_password, - }).success; - setStepValid(valid); - }); - return () => subscription.unsubscribe(); - }, [form]); - const onSubmit = form.handleSubmit((data) => { postRegister.mutate(data, { - onSuccess: (data) => toast.success(data.message), + onSuccess: () => toast.success('Registrasi sukses'), onError: (error) => toast.error(error.message), }); }); @@ -53,6 +31,5 @@ export const useRegister = () => { return { form, onSubmit, - isStepOneValid: stepValid, }; }; diff --git a/apps/gacha/src/middleware.ts b/apps/gacha/src/middleware.ts index f32b107..3d4d24a 100644 --- a/apps/gacha/src/middleware.ts +++ b/apps/gacha/src/middleware.ts @@ -1,11 +1,109 @@ -import { LoaderFunctionArgs } from 'react-router-dom'; +import { + PERMISSIONS, + SessionToken, + SessionUser, +} from '@imphnen-frontend-service/utils'; +import { LoaderFunctionArgs, redirect } from 'react-router'; + +const mappingPublicRoutes = [ + '/', + '/auth/login', + '/auth/forgot', + '/auth/new-password', +]; + +const mappingRoutePermissions = [ + { + path: '/dashboard', + permissions: [], + }, + { + path: '/users', + permissions: [PERMISSIONS.USERS.READ_LIST], + }, + { + path: '/users/create', + permissions: [PERMISSIONS.USERS.CREATE], + }, + { + path: '/users/update', + permissions: [PERMISSIONS.USERS.UPDATE], + }, + { + path: '/users/detail', + permissions: [PERMISSIONS.USERS.READ_DETAIL], + }, + { + path: '/roles', + permissions: [PERMISSIONS.ROLES.READ_LIST], + }, + { + path: '/roles/create', + permissions: [PERMISSIONS.ROLES.CREATE], + }, + { + path: '/roles/update', + permissions: [PERMISSIONS.ROLES.UPDATE], + }, + { + path: '/roles/detail', + permissions: [PERMISSIONS.ROLES.READ_DETAIL], + }, + { + path: '/permissions', + permissions: [PERMISSIONS.PERMISSIONS.READ_LIST], + }, + { + path: '/permissions/create', + permissions: [PERMISSIONS.PERMISSIONS.CREATE], + }, + { + path: '/permissions/update', + permissions: [PERMISSIONS.PERMISSIONS.UPDATE], + }, + { + path: '/permissions/detail', + permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL], + }, +]; + +//TODO : Fix this later +// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => { +// const fallback = mappingRoutePermissions.find((route) => +// route.permissions.some((perm) => userPermissions.includes(perm)) +// ); +// return redirect(fallback?.path ?? '/auth/login'); +// }; export const middleware = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); - const pathname = url.pathname; + const session = SessionUser.get(); + const session_token = SessionToken.get(); + const token = session_token?.token?.access_token; + const userPermissions = + session?.role?.permissions?.map?.((perm) => perm?.name) ?? []; - if (pathname) return null; + if (mappingPublicRoutes.includes(pathname)) { + if (token) return redirect('/dashboard'); + return null; + } + + if (!session) return redirect('/auth/login'); + + const matchedRoute = mappingRoutePermissions.find( + (route) => route.path === pathname + ); + + if (matchedRoute) { + const hasPermission = + !matchedRoute.permissions || + matchedRoute.permissions.some((perm) => userPermissions.includes(perm)); + + if (!hasPermission) { + return '/dashboard'; + } + } return null; }; diff --git a/libs/service/src/hooks/auth/index.ts b/libs/service/src/hooks/auth/index.ts index d4fd99e..4c2540b 100644 --- a/libs/service/src/hooks/auth/index.ts +++ b/libs/service/src/hooks/auth/index.ts @@ -6,7 +6,6 @@ import { TRegisterRequest, TVerifyEmailRequest, } from '../../types/auth'; -import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils'; import { TResponseError, TResponseMessage } from '../../types/common'; @@ -19,11 +18,6 @@ export const usePostLogin = (): UseMutationResult< return useMutation({ mutationKey: ['post-login'], mutationFn: async (payload) => await postLogin(payload), - onSuccess: (res) => { - SessionUser.set(res.data.user); - SessionToken.set(res.data.token); - window.location.reload(); - }, }); }; diff --git a/libs/service/src/schemas/auth/index.ts b/libs/service/src/schemas/auth/index.ts index d406f97..3f99256 100644 --- a/libs/service/src/schemas/auth/index.ts +++ b/libs/service/src/schemas/auth/index.ts @@ -16,7 +16,7 @@ export const authLoginSchema = z.object({ .min(1, 'Password tidak boleh kosong'), }); -export const stepOneRegisterSchema = z +export const authRegisterSchema = z .object({ email: z .string({ @@ -25,6 +25,12 @@ export const stepOneRegisterSchema = z }) .min(1, 'Email tidak boleh kosong') .email('Email harus valid'), + phone_number: z + .string({ + required_error: 'Nomor telepon tidak boleh kosong', + }) + .min(1, 'Nomor telepon tidak boleh kosong') + .max(15, 'Nomor telepon tidak boleh lebih dari 15 digit'), fullname: z .string({ required_error: 'Nama tidak boleh kosong', @@ -51,28 +57,3 @@ export const stepOneRegisterSchema = z message: 'Password dan Konfirmasi Password harus sama', path: ['confirm_password'], }); - -const stepTwoRegisterSchema = z.object({ - phone_number: z - .string({ - required_error: 'Nomor telepon tidak boleh kosong', - invalid_type_error: 'Nomor telepon harus berupa string', - }) - .min(1, 'Nomor telepon tidak boleh kosong') - .max(15, 'Nomor telepon tidak boleh lebih dari 15 karakter'), - referral_code: z - .string() - .min(1, 'Kode referral tidak boleh kosong') - .max(4, 'Kode referral tidak boleh lebih dari 4 karakter') - .optional(), - referred_by: z - .string() - .min(1, 'Kode referral tidak boleh kosong') - .max(50, 'Kode referral tidak boleh lebih dari 50 karakter') - .optional(), - student_type: z.string(), -}); - -export const authRegisterSchema = stepOneRegisterSchema.and( - stepTwoRegisterSchema -); diff --git a/libs/service/src/types/auth/index.ts b/libs/service/src/types/auth/index.ts index 8bd2d8d..874ad0f 100644 --- a/libs/service/src/types/auth/index.ts +++ b/libs/service/src/types/auth/index.ts @@ -1,29 +1,26 @@ +import { z } from 'zod'; +import { authLoginSchema, authRegisterSchema } from '../../schemas'; +import { TResponseDetail, TResponseMessage } from '../common'; import { TUserItem } from '../users'; -export type TLoginRequest = { - email: string; - password: string; +export type TTokenItem = { + access_token: string; + refresh_token: string; }; -export type TLoginResponse = { - data: { - token: { - access_token: string; - refresh_token: string; - }; - user: TUserItem; - }; +export type TLoginItem = { + token?: TTokenItem; + user?: TUserItem; }; -export type TRegisterRequest = { - email: string; - fullname: string; - password: string; - phone_number: string; - referral_code?: string; - referred_by?: string; - student_type: string; - confirm_password: string; +export type TLoginRequest = z.infer; +export type TLoginResponse = TResponseDetail; + +export type TRegisterRequest = z.infer; +export type TRegisterResponse = TResponseDetail; + +export type TVerifyOtpRequest = { + otp: number; }; export type TVerifyEmailRequest = { diff --git a/libs/service/src/types/common/index.ts b/libs/service/src/types/common/index.ts index b57f062..e6c509a 100644 --- a/libs/service/src/types/common/index.ts +++ b/libs/service/src/types/common/index.ts @@ -1,7 +1,19 @@ import { AxiosError } from 'axios'; -export type TResponse = { +export type TMetaResponse = { + page: number; + per_page: number; + total: number; +}; + +export type TResponseDetail = { data: T; + message: string; +}; + +export type TResponseList = { + data: T[]; + meta: TMetaResponse; }; export type TResponseMessage = { diff --git a/libs/service/src/types/roles/index.ts b/libs/service/src/types/roles/index.ts index 9a51667..5e6b8a5 100644 --- a/libs/service/src/types/roles/index.ts +++ b/libs/service/src/types/roles/index.ts @@ -1,9 +1,17 @@ import { TPermissionItem } from '../permissions'; -export type TRoleItem = { +export type TRoleDetailItem = { id: string; name: string; + permissions: TPermissionItem[]; + created_at: string; + updated_at: string; +}; + +export type TRolesListItem = { + id: string; + name: string; + permissions_count: number; created_at: string; updated_at: string; - permissions: TPermissionItem[]; }; diff --git a/libs/service/src/types/users/index.ts b/libs/service/src/types/users/index.ts index 8b6abae..854aba6 100644 --- a/libs/service/src/types/users/index.ts +++ b/libs/service/src/types/users/index.ts @@ -1,4 +1,4 @@ -import { TRoleItem } from '../roles'; +import { TRoleDetailItem } from '../roles'; export type TUserItem = { id: string; @@ -7,13 +7,7 @@ export type TUserItem = { email: string; fullname: string; gender: string; - identity_number: string; is_active: boolean; - is_profile_completed: boolean; phone_number: string; - referral_code: string; - referred_by: string; - religion: string; - student_type: string; - role: TRoleItem; + role: TRoleDetailItem; }; diff --git a/libs/utils/src/constants/index.ts b/libs/utils/src/constants/index.ts new file mode 100644 index 0000000..c85954d --- /dev/null +++ b/libs/utils/src/constants/index.ts @@ -0,0 +1 @@ +export * from './permissions'; diff --git a/libs/utils/src/constants/permissions.ts b/libs/utils/src/constants/permissions.ts new file mode 100644 index 0000000..ea335e8 --- /dev/null +++ b/libs/utils/src/constants/permissions.ts @@ -0,0 +1,40 @@ +export const PERMISSIONS = { + USERS: { + READ_LIST: 'Read List Users', + READ_DETAIL: 'Read Detail Users', + CREATE: 'Create Users', + UPDATE: 'Update Users', + DELETE: 'Delete Users', + ACTIVATE: 'Activate Users', + }, + ROLES: { + READ_LIST: 'Read List Roles', + READ_DETAIL: 'Read Detail Roles', + CREATE: 'Create Roles', + UPDATE: 'Update Roles', + DELETE: 'Delete Roles', + }, + PERMISSIONS: { + READ_LIST: 'Read List Permissions', + READ_DETAIL: 'Read Detail Permissions', + CREATE: 'Create Permissions', + UPDATE: 'Update Permissions', + DELETE: 'Delete Permissions', + }, + GACHA_CLAIMS: { + CREATE: 'Create Gacha Claims', + READ_DETAIL: 'Read Detail Gacha Claims', + }, + GACHA_ITEMS: { + READ_LIST: 'Read List Gacha Items', + READ_DETAIL: 'Read Detail Gacha Items', + CREATE: 'Create Gacha Items', + UPDATE: 'Update Gacha Items', + DELETE: 'Delete Gacha Items', + }, + GACHA_ROLLS: { + READ_DETAIL: 'Read Detail Gacha Rolls', + CREATE: 'Create Gacha Rolls', + EXECUTE: 'Execute Gacha Rolls', + }, +}; diff --git a/libs/utils/src/cookies/index.ts b/libs/utils/src/cookies/index.ts new file mode 100644 index 0000000..457c2c1 --- /dev/null +++ b/libs/utils/src/cookies/index.ts @@ -0,0 +1,36 @@ +import Cookies from 'js-cookie'; + +type TTokenItem = { + access_token: string; + refresh_token: string; +}; + +const TOKEN_KEY = 'token'; + +type TStoredToken = + | { + token?: TTokenItem; + } + | undefined; + +export const SessionToken = { + set: (val: TStoredToken) => { + Cookies.set(TOKEN_KEY, JSON.stringify(val), { + secure: true, + sameSite: 'Strict', + expires: 7, + }); + }, + get: (): TStoredToken => { + const token = Cookies.get(TOKEN_KEY); + if (!token) return undefined; + try { + return JSON.parse(token); + } catch { + return undefined; + } + }, + remove: () => { + Cookies.remove(TOKEN_KEY); + }, +}; diff --git a/libs/utils/src/hooks/index.ts b/libs/utils/src/hooks/index.ts index 6b7067f..2f20b90 100644 --- a/libs/utils/src/hooks/index.ts +++ b/libs/utils/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './use-query-state'; -export * from './use-session'; +export * from './use-auth-store'; export * from './use-modal-login'; +export * from './use-session'; diff --git a/libs/utils/src/hooks/use-auth-store.ts b/libs/utils/src/hooks/use-auth-store.ts new file mode 100644 index 0000000..b5d0e39 --- /dev/null +++ b/libs/utils/src/hooks/use-auth-store.ts @@ -0,0 +1,47 @@ +import { create } from 'zustand'; +import { TLoginItem } from '@imphnen-frontend-service/service'; +import { SessionToken } from '../cookies'; +import { SessionUser } from '../local-storage'; + +export enum ESessionStatus { + Authenticated = 'authenticated', + Authenticating = 'authenticating', + Unauthenticated = 'unauthenticated', +} + +type SessionState = { + isLoading: boolean; + session?: TLoginItem; + status?: ESessionStatus; + setLoading: (val: boolean) => void; + setSession: (payload: TLoginItem) => void; + clearSession: () => void; +}; + +export const useAuthStore = create((set) => { + const session = SessionToken.get(); + const user = SessionUser.get(); + const isAuthenticated = !!session; + + return { + isLoading: false, + session: isAuthenticated ? { ...session, ...user } : undefined, + status: isAuthenticated + ? ESessionStatus.Authenticated + : ESessionStatus.Unauthenticated, + setLoading: (val) => set({ isLoading: val }), + setSession: (data) => { + SessionToken.set({ token: data.token }); + SessionUser.set(data.user); + set({ + session: data, + status: ESessionStatus.Authenticated, + }); + }, + clearSession: () => { + SessionToken.remove(); + SessionUser.remove(); + set({ session: undefined, status: ESessionStatus.Unauthenticated }); + }, + }; +}); diff --git a/libs/utils/src/hooks/use-session.ts b/libs/utils/src/hooks/use-session.ts index 880ec1f..ac47c71 100644 --- a/libs/utils/src/hooks/use-session.ts +++ b/libs/utils/src/hooks/use-session.ts @@ -1,22 +1,37 @@ -import { SessionToken, SessionUser } from '../local-storage'; +import { TLoginRequest, usePostLogin } from '@imphnen-frontend-service/service'; +import { useAuthStore } from './'; +import { toast } from 'sonner'; +import { useNavigate } from 'react-router'; export const useSession = () => { - const session = { - user: SessionUser.get(), - token: SessionToken.get(), + const navigate = useNavigate(); + const { mutate, isPending } = usePostLogin(); + const { setLoading, setSession, clearSession, session } = useAuthStore(); + const signIn = (payload: TLoginRequest) => { + setLoading(true); + mutate(payload, { + onSuccess: (data) => { + toast.success('Login sukses'); + setSession(data.data); + navigate(0); + }, + onError: (err) => { + toast.error( + err?.response?.data?.message ?? + 'Terjadi Kesalahan yang tidak diketahui' + ); + clearSession(); + }, + }); }; - - const isAuthenticated = !!session.token?.access_token; - const signOut = () => { - SessionUser.remove(); - SessionToken.remove(); - window.location.reload(); + clearSession(); + navigate(0); }; - return { - isAuthenticated, session, + signIn, signOut, + isLoading: isPending, }; }; diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts index 5884fb4..722f7b8 100644 --- a/libs/utils/src/index.ts +++ b/libs/utils/src/index.ts @@ -3,3 +3,6 @@ export * from './react-router'; export * from './tailwind-merge'; export * from './hooks'; export * from './local-storage'; +export * from './cookies'; +export * from './session'; +export * from './constants'; diff --git a/libs/utils/src/local-storage/index.ts b/libs/utils/src/local-storage/index.ts index 9cd2959..893120e 100644 --- a/libs/utils/src/local-storage/index.ts +++ b/libs/utils/src/local-storage/index.ts @@ -20,41 +20,16 @@ type TUserItem = { email: string; fullname: string; gender: string; - identity_number: string; is_active: boolean; - is_profile_completed: boolean; phone_number: string; - referral_code: string; - referred_by: string; - religion: string; - student_type: string; role: TRoleItem; }; export const SessionUser = { - set: (val: TUserItem) => localStorage.setItem('users', JSON.stringify(val)), + set: (val?: TUserItem) => localStorage.setItem('users', JSON.stringify(val)), get: (): TUserItem | undefined => { const users = localStorage.getItem('users'); return users ? JSON.parse(users) : undefined; }, remove: () => localStorage.removeItem('users'), }; - -export const SessionToken = { - set: (val: { access_token: string; refresh_token: string }) => { - localStorage.setItem('access_token', val.access_token); - localStorage.setItem('refresh_token', val.refresh_token); - }, - get: (): - | { access_token?: string | null; refresh_token?: string | null } - | undefined => { - return { - access_token: localStorage.getItem('access_token'), - refresh_token: localStorage.getItem('refresh_token'), - }; - }, - remove: () => { - localStorage.removeItem('access_token'); - localStorage.removeItem('refresh_token'); - }, -}; diff --git a/libs/utils/src/react-router/file-based-routing.tsx b/libs/utils/src/react-router/file-based-routing.tsx index 9ad1132..b8c6d73 100644 --- a/libs/utils/src/react-router/file-based-routing.tsx +++ b/libs/utils/src/react-router/file-based-routing.tsx @@ -1,15 +1,22 @@ -import { lazy, LazyExoticComponent, ReactElement } from 'react'; +import { lazy, LazyExoticComponent, ReactNode } from 'react'; import { ActionFunction, LoaderFunction, RouteObject } from 'react-router'; +type TPermissionItem = { + id: string; + name: string; + created_at: string; + updated_at: string; +}; + interface PageModuleExports { - default: () => ReactElement; + default: () => ReactNode; loader?: LoaderFunction; action?: ActionFunction; permissions?: Array; } interface LoadingModuleExports { - default: () => ReactElement; + default: () => ReactNode; } interface RouteHandle { @@ -38,7 +45,6 @@ export function convertPagesToRoute( filePath, loadingFiles ); - const route = createRoute({ PageComponent: page, LoadingComponent: loadingComponent, @@ -51,6 +57,20 @@ export function convertPagesToRoute( const result = (await importer()) as PageModuleExports; return 'loader' in result ? result.loader?.(args) : null; }, + async guard() { + const result = (await importer()) as PageModuleExports; + const localStoragePermission = localStorage.getItem('permissions'); + const permissions: TPermissionItem[] | undefined = + localStoragePermission + ? JSON.parse(localStoragePermission) + : undefined; + return 'permissions' in result + ? result.permissions?.every( + (permission) => + permissions?.some((item) => permission === item.name) || false + ) || false + : true; + }, }); routes = mergeRoutes(routes, route); }); @@ -62,19 +82,14 @@ function findMatchingLoadingComponent( loadingFiles: Record Promise> ) { const loadingPath = filePath.replace(/(page|layout)\.tsx$/, 'loading.tsx'); - const groupMatch = filePath.match(/\([^/]+\//); const groupLoadingPath = groupMatch ? `/${groupMatch[0]}loading.tsx` : null; - const globalLoadingPath = './app/loading.tsx'; - const loader = loadingFiles[loadingPath] || (groupLoadingPath && loadingFiles[groupLoadingPath]) || loadingFiles[globalLoadingPath]; - if (!loader) return undefined; - return lazy(loader as () => Promise); } @@ -86,68 +101,110 @@ function mergeRoutes( throw new Error( `Paths do not match: "${target.path}" and "${source.path}"` ); + target.children = target.children || []; + if (source.handle?.pageType === 'layout') { + return handleLayoutMerge(target, source); + } + if (source.handle?.pageType === 'page') { + return handlePageMerge(target, source); + } + if (source.children && source.children.length > 0) { + if (target.handle?.pageType === 'page') { + if (!target.children?.some((child) => child.index)) { + target.children = target.children || []; + target.children.unshift({ + index: true, + element: target.element, + HydrateFallback: target.HydrateFallback, + action: target.action, + loader: target.loader, + handle: target.handle, + errorElement: target.errorElement, + }); + } + delete target.element; + delete target.action; + delete target.loader; + delete target.handle; + } + mergeChildRoutes(target, source); + } + return target; +} +export function mergeChildRoutes( + target: ExtendedRouteObject, + source: ExtendedRouteObject +): void { + if (!source.children) return; if (!target.children) { target.children = []; } + source.children.forEach((sourceChild) => { + const matchingChild = target.children!.find( + (targetChild) => targetChild.path === sourceChild.path + ); - if (source.handle?.pageType === 'layout') { - if (!target.element) { - target.element = source.element; - target.HydrateFallback = source.HydrateFallback; - target.action = source.action; - target.loader = source.loader; - target.handle = source.handle; - target.errorElement = source.errorElement; - target.children = target.children ?? []; - } else if (target.handle?.pageType === 'page') { - target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source); + if (matchingChild) { + mergeRoutes(matchingChild, sourceChild); + } else { + target.children!.push(sourceChild); } - return target; - } + }); +} - if ( - source.handle?.pageType === 'page' && - !target.children.some((child) => child.index) - ) { - target.children.unshift({ - index: true, +export function handleLayoutMerge( + target: ExtendedRouteObject, + source: ExtendedRouteObject +): ExtendedRouteObject { + if (!target.element) { + Object.assign(target, { element: source.element, HydrateFallback: source.HydrateFallback, action: source.action, loader: source.loader, handle: source.handle, + errorElement: source.errorElement, }); - return target; + } else if (target.handle?.pageType === 'page') { + target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source); } - - if ( - target.handle?.pageType === 'layout' && - source.handle?.pageType === 'page' - ) { - target = addRouteAsIndexRouteForTargetRoute(target, source); - return target; - } - - if (source.children) { - target.children = target.children ?? []; - source.children.forEach((sourceChild) => { - const matchingChild = target.children?.find( - (targetChild) => targetChild.path === sourceChild.path - ); - if (matchingChild) mergeRoutes(matchingChild, sourceChild); - else target.children?.push(sourceChild); - }); - } - return target; } -function swapTargetRouteAsIndexRouteAndUpdateWithRoute( +export function handlePageMerge( target: ExtendedRouteObject, - route: ExtendedRouteObject + source: ExtendedRouteObject ): ExtendedRouteObject { - target.children = target.children ?? []; + if (!target.children) { + target.children = []; + } + if ( + !target.children.some((child) => child.index) || + target.handle?.pageType === 'layout' + ) { + if (target.handle?.pageType === 'layout') { + addRouteAsIndexRouteForTargetRoute(target, source); + } else { + target.children.unshift({ + index: true, + element: source.element, + HydrateFallback: source.HydrateFallback, + action: source.action, + loader: source.loader, + handle: source.handle, + errorElement: source.errorElement, + }); + } + } + return target; +} + +export function swapTargetRouteAsIndexRouteAndUpdateWithRoute( + target: ExtendedRouteObject, + layout: ExtendedRouteObject +): ExtendedRouteObject { + target.children = target.children || []; target.children.push({ index: true, element: target.element, @@ -157,38 +214,38 @@ function swapTargetRouteAsIndexRouteAndUpdateWithRoute( handle: target.handle, errorElement: target.errorElement, }); - - target.element = route.element; - target.HydrateFallback = route.HydrateFallback; - target.action = route.action; - target.loader = route.loader; - target.handle = route.handle; - target.errorElement = route.errorElement; - + Object.assign(target, { + element: layout.element, + HydrateFallback: layout.HydrateFallback, + action: layout.action, + loader: layout.loader, + handle: layout.handle, + errorElement: layout.errorElement, + }); return target; } -function addRouteAsIndexRouteForTargetRoute( +export function addRouteAsIndexRouteForTargetRoute( target: ExtendedRouteObject, - route: ExtendedRouteObject + page: ExtendedRouteObject ): ExtendedRouteObject { - target.children = target.children ?? []; + target.children = target.children || []; target.children.push({ index: true, - element: route.element, - HydrateFallback: route.HydrateFallback, - action: route.action, - loader: route.loader, - handle: route.handle, - errorElement: route.errorElement, + element: page.element, + HydrateFallback: page.HydrateFallback, + action: page.action, + loader: page.loader, + handle: page.handle, + errorElement: page.errorElement, }); return target; } function createRoute(args: { segments: string[]; - PageComponent: LazyExoticComponent<() => ReactElement>; - LoadingComponent?: LazyExoticComponent<() => ReactElement>; + PageComponent: LazyExoticComponent<() => ReactNode>; + LoadingComponent?: LazyExoticComponent<() => ReactNode>; loader?: LoaderFunction; action?: ActionFunction; guard?: () => Promise; @@ -196,21 +253,24 @@ function createRoute(args: { const [current, ...rest] = args.segments; const [cleanPath, pageType] = current.split(separator); const route: ExtendedRouteObject = { path: cleanPath }; - if (pageType === 'page' || pageType === 'layout') { route.element = ; route.HydrateFallback = args.LoadingComponent ?? (() =>
Loading...
); route.action = args.action; route.loader = async (...props) => { + if (!(await args.guard?.())) { + throw new Response('Forbidden', { + status: 403, + statusText: 'Forbidden', + }); + } return args.loader?.(...props); }; route.handle = { pageType: pageType as 'layout' | 'page' }; } - if (rest.length > 0) { const nextSegment = rest[0].split(separator)[0]; - if (nextSegment === 'update' || nextSegment === 'edit') { return { path: `${cleanPath}/${nextSegment}`, @@ -221,20 +281,16 @@ function createRoute(args: { handle: { pageType: pageType as 'layout' | 'page' }, }; } - const childRoute = createRoute({ ...args, segments: rest }); - if (!route.children) { route.children = []; } - if (cleanPath.startsWith(':')) { route.children.unshift(childRoute); } else { route.children.push(childRoute); } } - return route; } @@ -258,7 +314,6 @@ export function getRouteSegmentsFromFilePath( if (segment.startsWith('[')) return getParamFromSegment(segment); return segment; }); - return getRouteSegments(segments[0], segments, transformer); } @@ -306,7 +361,7 @@ export function addErrorElementToRoutes( (_, prevSegment) => prevSegment ); const ErrorBoundary = lazy( - importer as () => Promise<{ default: () => ReactElement }> + importer as () => Promise<{ default: () => ReactNode }> ); setRoute(segments, routes, (route) => { route.errorElement = ; @@ -325,7 +380,7 @@ export function add404PageToRoutesChildren( (_, prevSegment) => prevSegment ); const NotFound = lazy( - importer as () => Promise<{ default: () => ReactElement }> + importer as () => Promise<{ default: () => ReactNode }> ); setRoute(segments, routes, (route) => { if (route.children) { @@ -340,9 +395,7 @@ export function add404PageToRoutesChildren( action: tempRoute.action, loader: tempRoute.loader, }); - route.children.push({ path: '*', element: }); - delete route.element; delete route.action; delete route.loader; @@ -352,7 +405,7 @@ export function add404PageToRoutesChildren( }); } -function set404NonPage(routes: RouteObject, notFoundElement: ReactElement) { +function set404NonPage(routes: RouteObject, notFoundElement: ReactNode) { if ( routes.path && routes.children?.length && @@ -377,7 +430,6 @@ function setRoute( segments.forEach((_segment, i) => { const isLastSegment = i === segments.length - 1; if (isLastSegment) return (temp = updater(temp)); - if (!isLastSegment) { const nextSegment = segments[i + 1]; const index = temp.children?.findIndex( diff --git a/libs/utils/src/session/guard/index.tsx b/libs/utils/src/session/guard/index.tsx new file mode 100644 index 0000000..4fda160 --- /dev/null +++ b/libs/utils/src/session/guard/index.tsx @@ -0,0 +1,27 @@ +import { FC, PropsWithChildren, ReactNode, useMemo } from 'react'; +import { useSession } from '../../hooks/use-session'; + +type TProps = PropsWithChildren<{ + permissions: Array; + fallback?: ReactNode; +}>; + +export const Guard: FC = (props): ReactNode => { + const { session } = useSession(); + + const permissionKeys = useMemo(() => { + return session?.user?.role?.permissions.map((perm) => perm.name) ?? []; + }, [session?.user?.role]); + + const allowed = useMemo(() => { + return props.permissions.every((permission) => + permissionKeys.includes(permission) + ); + }, [permissionKeys, props.permissions]); + + if (allowed) { + return props.children; + } + + return props.fallback ?? null; +}; diff --git a/libs/utils/src/session/index.ts b/libs/utils/src/session/index.ts new file mode 100644 index 0000000..def4962 --- /dev/null +++ b/libs/utils/src/session/index.ts @@ -0,0 +1 @@ +export * from './guard'; diff --git a/package-lock.json b/package-lock.json index c5d83b2..abc1730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,8 @@ "sharp": "^0.34.1", "sonner": "^2.0.3", "tailwind-merge": "^3.0.2", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.5" }, "devDependencies": { "@babel/core": "^7.14.5", @@ -40387,6 +40388,35 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 6069068..335d473 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,15 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "gacha:dev": "nx serve gacha", + "gacha:dev": "nx dev gacha", "gacha:build": "nx build gacha", "gacha:test": "nx test gacha", "gacha:prod": "serve ./dist/apps/gacha", - "backoffice:dev": "nx serve backoffice", + "backoffice:dev": "nx dev backoffice", "backoffice:build": "nx build backoffice", "backoffice:test": "nx test backoffice", "backoffice:prod": "serve ./dist/apps/backoffice", - "dimentorin:dev": "nx serve dimentorin", + "dimentorin:dev": "nx dev dimentorin", "dimentorin:test": "nx test dimentorin", "dimentorin:build": "nx build dimentorin", "dimentorin:prod": "serve ./dist/apps/dimentorin", @@ -55,7 +55,8 @@ "sharp": "^0.34.1", "sonner": "^2.0.3", "tailwind-merge": "^3.0.2", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.5" }, "devDependencies": { "@babel/core": "^7.14.5",