Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3930542182 | ||
|
|
6f45ca295b | ||
|
|
9c4ce1cca9 |
@@ -1,5 +1,13 @@
|
|||||||
|
# API Endpoint
|
||||||
|
VITE_API_URL=
|
||||||
|
|
||||||
|
# Payload CMS Secret Key (minimum 32 characters)
|
||||||
CMS_SECRET=your-very-long-secret-key
|
CMS_SECRET=your-very-long-secret-key
|
||||||
|
|
||||||
|
# PostgreSQL Connection String
|
||||||
CMS_POSTGRES_URL=postgres://user:password@host:port/database
|
CMS_POSTGRES_URL=postgres://user:password@host:port/database
|
||||||
|
|
||||||
|
# S3 Storage Configuration
|
||||||
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
|
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
|
||||||
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
|
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
|
||||||
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
|
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
|
||||||
@@ -6,10 +6,9 @@
|
|||||||
|
|
||||||
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
|
||||||
|
|
||||||
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Gacha Website</a>.
|
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>.
|
||||||
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
2. **Backoffice** - Application for internal management.
|
||||||
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
3. **Dimentorin** - Application for mentoring services.
|
||||||
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -25,15 +24,6 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
|
|||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
### Setup Environment Variables
|
|
||||||
|
|
||||||
Before running the applications, you need to set up the environment variables. You can do this by copying the `.env.example` file to `.env` and modifying the values according to your needs.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd apps/{appname}
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
Use the following commands to run in development mode:
|
Use the following commands to run in development mode:
|
||||||
@@ -50,10 +40,6 @@ Use the following commands to run in development mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:dev
|
npm run dimentorin:dev
|
||||||
```
|
```
|
||||||
- **Landing Page**:
|
|
||||||
```sh
|
|
||||||
npm run landing:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
@@ -71,10 +57,6 @@ Use the following commands to build the applications:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:build
|
npm run dimentorin:build
|
||||||
```
|
```
|
||||||
- **Landing Page**:
|
|
||||||
```sh
|
|
||||||
npm run landing:build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
@@ -92,22 +74,19 @@ Use the following commands to run the applications in production mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run dimentorin:prod
|
npm run dimentorin:prod
|
||||||
```
|
```
|
||||||
- **Landing Page**:
|
|
||||||
```sh
|
|
||||||
npm run landing:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
|
||||||
|
|
||||||
- **Run Storybook**
|
- **Run Storybook**
|
||||||
|
|
||||||
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
This command starts Storybook in development mode, allowing you to view and test UI components interactively.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run ui:storybook
|
npm run ui:storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- **Run Unit Test**
|
- **Run Unit Test**
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=
|
|
||||||
+9
-4
@@ -2,19 +2,24 @@ import { useForm } from 'react-hook-form';
|
|||||||
import {
|
import {
|
||||||
authLoginSchema,
|
authLoginSchema,
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
|
usePostLogin,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useSession } from '@imphnen-frontend-service/utils';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
|
const postLogin = usePostLogin();
|
||||||
const form = useForm<TLoginRequest>({
|
const form = useForm<TLoginRequest>({
|
||||||
resolver: zodResolver(authLoginSchema),
|
resolver: zodResolver(authLoginSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { signIn } = useSession();
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postLogin.mutate(data, {
|
||||||
const onSubmit = form.handleSubmit((data) => signIn(data));
|
onSuccess: () => toast.success("Login sukses"),
|
||||||
|
onError: (error) => toast.error(error.message),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
+2
-2
@@ -18,7 +18,7 @@ import {
|
|||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import ModalEditAccount from './_components/modal-edit-account';
|
import ModalEditAccount from './_components/modal-edit-account';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '../../hook/use-query-state';
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
{showFilter && (
|
{showFilter && (
|
||||||
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
|
||||||
<Filter onClose={() => setShowFilter(false)} options={[]} />
|
<Filter onClose={() => setShowFilter(false)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react';
|
|||||||
import ModalAddItem from './_components/modal-add-item';
|
import ModalAddItem from './_components/modal-add-item';
|
||||||
import ModalEditItem from './_components/modal-edit-item';
|
import ModalEditItem from './_components/modal-edit-item';
|
||||||
import ModalDeleteItem from './_components/modal-delete-item';
|
import ModalDeleteItem from './_components/modal-delete-item';
|
||||||
import { useQueryState } from '@imphnen-frontend-service/utils';
|
import { useQueryState } from '../../hook/use-query-state';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
const [showModalAddItem, setShowModalAddItem] = useState(false);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
+1
-10
@@ -32,16 +32,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button type="submit" size="md" className="w-full">
|
||||||
disabled={
|
|
||||||
form.formState.isSubmitting ||
|
|
||||||
form.formState.isValidating ||
|
|
||||||
!form.formState.isValid
|
|
||||||
}
|
|
||||||
type="submit"
|
|
||||||
size="md"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 (
|
||||||
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
|
<BackofficeSidebar />
|
||||||
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -1,108 +1,11 @@
|
|||||||
import {
|
import { LoaderFunctionArgs } from 'react-router-dom';
|
||||||
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) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const pathname = url.pathname;
|
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 (mappingPublicRoutes.includes(pathname)) {
|
if (pathname) return null;
|
||||||
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=
|
|
||||||
@@ -2,19 +2,24 @@ import { useForm } from 'react-hook-form';
|
|||||||
import {
|
import {
|
||||||
authLoginSchema,
|
authLoginSchema,
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
|
usePostLogin,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useSession } from '@imphnen-frontend-service/utils';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
|
const postLogin = usePostLogin();
|
||||||
const form = useForm<TLoginRequest>({
|
const form = useForm<TLoginRequest>({
|
||||||
resolver: zodResolver(authLoginSchema),
|
resolver: zodResolver(authLoginSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { signIn } = useSession();
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postLogin.mutate(data, {
|
||||||
const onSubmit = form.handleSubmit((data) => signIn(data));
|
onSuccess: () => toast.success("Login sukses"),
|
||||||
|
onError: (error) => toast.error(error.message),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
|
|||||||
@@ -1,109 +1,11 @@
|
|||||||
import {
|
import { LoaderFunctionArgs } from 'react-router-dom';
|
||||||
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) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const pathname = url.pathname;
|
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 (mappingPublicRoutes.includes(pathname)) {
|
if (pathname) return null;
|
||||||
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_URL=
|
|
||||||
@@ -2,19 +2,22 @@ import { useForm } from 'react-hook-form';
|
|||||||
import {
|
import {
|
||||||
authLoginSchema,
|
authLoginSchema,
|
||||||
TLoginRequest,
|
TLoginRequest,
|
||||||
|
usePostLogin,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useSession } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
|
const postLogin = usePostLogin();
|
||||||
const form = useForm<TLoginRequest>({
|
const form = useForm<TLoginRequest>({
|
||||||
resolver: zodResolver(authLoginSchema),
|
resolver: zodResolver(authLoginSchema),
|
||||||
mode: 'all',
|
mode: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { signIn } = useSession();
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
|
postLogin.mutate(data, {
|
||||||
const onSubmit = form.handleSubmit((data) => signIn(data));
|
onSuccess: () => console.log('Success Login'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
authRegisterSchema,
|
authRegisterSchema,
|
||||||
|
stepOneRegisterSchema,
|
||||||
TRegisterRequest,
|
TRegisterRequest,
|
||||||
usePostRegister,
|
usePostRegister,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { z } from 'zod';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useRegister = () => {
|
export const useRegister = () => {
|
||||||
const postRegister = usePostRegister();
|
const postRegister = usePostRegister();
|
||||||
@@ -18,12 +21,31 @@ export const useRegister = () => {
|
|||||||
password: '',
|
password: '',
|
||||||
confirm_password: '',
|
confirm_password: '',
|
||||||
phone_number: '',
|
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) => {
|
const onSubmit = form.handleSubmit((data) => {
|
||||||
postRegister.mutate(data, {
|
postRegister.mutate(data, {
|
||||||
onSuccess: () => toast.success('Registrasi sukses'),
|
onSuccess: (data) => toast.success(data.message),
|
||||||
onError: (error) => toast.error(error.message),
|
onError: (error) => toast.error(error.message),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,5 +53,6 @@ export const useRegister = () => {
|
|||||||
return {
|
return {
|
||||||
form,
|
form,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
isStepOneValid: stepValid,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,109 +1,11 @@
|
|||||||
import {
|
import { LoaderFunctionArgs } from 'react-router-dom';
|
||||||
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) => {
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const pathname = url.pathname;
|
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 (mappingPublicRoutes.includes(pathname)) {
|
if (pathname) return null;
|
||||||
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TRegisterRequest,
|
TRegisterRequest,
|
||||||
TVerifyEmailRequest,
|
TVerifyEmailRequest,
|
||||||
} from '../../types/auth';
|
} from '../../types/auth';
|
||||||
|
import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
import { TResponseError, TResponseMessage } from '../../types/common';
|
import { TResponseError, TResponseMessage } from '../../types/common';
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ export const usePostLogin = (): UseMutationResult<
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['post-login'],
|
mutationKey: ['post-login'],
|
||||||
mutationFn: async (payload) => await postLogin(payload),
|
mutationFn: async (payload) => await postLogin(payload),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
SessionUser.set(res.data.user);
|
||||||
|
SessionToken.set(res.data.token);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const authLoginSchema = z.object({
|
|||||||
.min(1, 'Password tidak boleh kosong'),
|
.min(1, 'Password tidak boleh kosong'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authRegisterSchema = z
|
export const stepOneRegisterSchema = z
|
||||||
.object({
|
.object({
|
||||||
email: z
|
email: z
|
||||||
.string({
|
.string({
|
||||||
@@ -25,12 +25,6 @@ export const authRegisterSchema = z
|
|||||||
})
|
})
|
||||||
.min(1, 'Email tidak boleh kosong')
|
.min(1, 'Email tidak boleh kosong')
|
||||||
.email('Email harus valid'),
|
.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
|
fullname: z
|
||||||
.string({
|
.string({
|
||||||
required_error: 'Nama tidak boleh kosong',
|
required_error: 'Nama tidak boleh kosong',
|
||||||
@@ -57,3 +51,28 @@ export const authRegisterSchema = z
|
|||||||
message: 'Password dan Konfirmasi Password harus sama',
|
message: 'Password dan Konfirmasi Password harus sama',
|
||||||
path: ['confirm_password'],
|
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
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { authLoginSchema, authRegisterSchema } from '../../schemas';
|
|
||||||
import { TResponseDetail, TResponseMessage } from '../common';
|
|
||||||
import { TUserItem } from '../users';
|
import { TUserItem } from '../users';
|
||||||
|
|
||||||
export type TTokenItem = {
|
export type TLoginRequest = {
|
||||||
access_token: string;
|
email: string;
|
||||||
refresh_token: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLoginItem = {
|
export type TLoginResponse = {
|
||||||
token?: TTokenItem;
|
data: {
|
||||||
user?: TUserItem;
|
token: {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
user: TUserItem;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLoginRequest = z.infer<typeof authLoginSchema>;
|
export type TRegisterRequest = {
|
||||||
export type TLoginResponse = TResponseDetail<TLoginItem>;
|
email: string;
|
||||||
|
fullname: string;
|
||||||
export type TRegisterRequest = z.infer<typeof authRegisterSchema>;
|
password: string;
|
||||||
export type TRegisterResponse = TResponseDetail<TResponseMessage>;
|
phone_number: string;
|
||||||
|
referral_code?: string;
|
||||||
export type TVerifyOtpRequest = {
|
referred_by?: string;
|
||||||
otp: number;
|
student_type: string;
|
||||||
|
confirm_password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyEmailRequest = {
|
export type TVerifyEmailRequest = {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user