diff --git a/apps/gacha/src/app/_components/form/modal-form-login.tsx b/apps/gacha/src/app/_components/form/modal-form-login.tsx index 3b2c6e8..e81ee8b 100644 --- a/apps/gacha/src/app/_components/form/modal-form-login.tsx +++ b/apps/gacha/src/app/_components/form/modal-form-login.tsx @@ -2,6 +2,7 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Modal } from '@imphnen-frontend-service/ui/molecules'; import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; import { useLogin } from '../../_hooks/use-login'; +import ModalFormVerifyEmail from './modal-form-verify-email'; interface IModalFormLogin { isOpen: boolean; @@ -16,61 +17,87 @@ const ModalFormLogin = ({ onForgotPassword, setIsOpenRegisterModal, }: IModalFormLogin) => { - const { form, onSubmit } = useLogin(); + const { + form, + onSubmit, + isLoading, + showVerifyModal, + verifyForm, + onVerifySubmit, + closeVerifyModal, + isVerifying, + emailToVerify, + } = useLogin(); return ( - - - -

- Login -

-
- -
- - -

- -

- -
-

Belum punya akun?

+ + + + + + +

+ +

-
- -
-
+
+

Belum punya akun?

+ +
+ + + + + + ); }; diff --git a/apps/gacha/src/app/_components/form/modal-form-register.tsx b/apps/gacha/src/app/_components/form/modal-form-register.tsx index 5eb5c82..2a51125 100644 --- a/apps/gacha/src/app/_components/form/modal-form-register.tsx +++ b/apps/gacha/src/app/_components/form/modal-form-register.tsx @@ -1,11 +1,8 @@ import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Modal } from '@imphnen-frontend-service/ui/molecules'; -import { useQueryState } from '@imphnen-frontend-service/utils'; import { useRegister } from '../../_hooks/use-register'; import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; -import { UseFormReturn } from 'react-hook-form'; -import { TRegisterRequest } from '@imphnen-frontend-service/service'; -import { useEffect } from 'react'; +import ModalFormVerifyEmail from './modal-form-verify-email'; interface IModalFormRegisterProps { isOpen: boolean; @@ -13,168 +10,100 @@ interface IModalFormRegisterProps { } const ModalFormRegister = ({ isOpen, onClose }: IModalFormRegisterProps) => { - const { form, onSubmit, isStepOneValid } = useRegister(); - const { - step: currentStep, - nextStep, - prevStep, - resetStep, - } = useQueryState('registerStep', { - defaultValue: 1, - maxValue: 2, - minValue: 1, - }); + form, + verifyForm, + onSubmit, + onVerifySubmit, + showVerifyModal, + closeVerifyModal, + isVerifying, + isRegistering, + registeredEmail, + } = useRegister(); - useEffect(() => { - if (!isStepOneValid && currentStep === 2) { - prevStep(); - } - }, [isStepOneValid, currentStep, prevStep]); - - return ( - { - onClose(); - resetStep(); - }} - > - - -

- Register -

-

- {currentStep === 1 ? 'Info Akun' : 'Informasi Pengiriman Hadiah'} -

-
- -
- {currentStep === 1 && ( - - )} - {currentStep === 2 && } - -
-
- ); -}; - -interface IStepOneProps { - form: UseFormReturn; - nextStep: () => void; - isStepOneValid: boolean; -} - -const StepOne = ({ form, nextStep, isStepOneValid }: IStepOneProps) => { return ( <> - - - - - + + +

+ Register +

+
+ +
+ + + + + + + +
+ + + ); }; -interface IStepTwoProps { - form: UseFormReturn; - prevStep: () => void; -} - -const StepTwo = ({ form, prevStep }: IStepTwoProps) => ( - <> - - - - -
- - -
- -); - export default ModalFormRegister; diff --git a/apps/gacha/src/app/_components/form/modal-form-verify-email.tsx b/apps/gacha/src/app/_components/form/modal-form-verify-email.tsx new file mode 100644 index 0000000..c81f679 --- /dev/null +++ b/apps/gacha/src/app/_components/form/modal-form-verify-email.tsx @@ -0,0 +1,63 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; +import { UseFormReturn } from 'react-hook-form'; + +interface IModalFormVerifyEmailProps { + isOpen: boolean; + onClose: () => void; + verifyForm: UseFormReturn<{ otp: string }, any>; + onVerifySubmit: (e: React.FormEvent) => void; + isVerifying: boolean; + email?: string; +} + +const ModalFormVerifyEmail = ({ + isOpen, + onClose, + verifyForm, + onVerifySubmit, + isVerifying, + email, +}: IModalFormVerifyEmailProps) => { + return ( + + + +

+ Verifikasi Email +

+

+ Masukkan kode OTP yang telah dikirim ke {email ? email : 'email Anda'} +

+
+ +
+ + + +
+
+ ); +}; + +export default ModalFormVerifyEmail; diff --git a/apps/gacha/src/app/_hooks/use-login.ts b/apps/gacha/src/app/_hooks/use-login.ts index e0323ce..05ed626 100644 --- a/apps/gacha/src/app/_hooks/use-login.ts +++ b/apps/gacha/src/app/_hooks/use-login.ts @@ -2,9 +2,13 @@ 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'; +import { useAuthStore } from '@imphnen-frontend-service/utils'; +import { toast } from 'sonner'; +import { useNavigate } from 'react-router'; +import { useVerifyEmail } from './use-verify-email'; export const useLogin = () => { const form = useForm({ @@ -12,12 +16,63 @@ export const useLogin = () => { mode: 'all', }); - const { signIn } = useSession(); + const navigate = useNavigate(); + const { mutate, isPending } = usePostLogin(); + const { setLoading, setSession, clearSession } = useAuthStore(); - const onSubmit = form.handleSubmit((data) => signIn(data)); + const { + openVerifyModal, + showVerifyModal, + verifyForm, + onVerifySubmit, + closeVerifyModal, + isVerifying, + emailToVerify, + } = useVerifyEmail(); + + const onSubmit = form.handleSubmit((data) => { + setLoading(true); + mutate(data, { + onSuccess: (response) => { + toast.success('Login sukses'); + + if (response.data.token && response.data.user) { + setSession(response.data); + } else { + const { token, ...userData } = response.data; + + const loginData = { + token, + userData, + }; + setSession(loginData); + } + + navigate(0); + }, + onError: (error) => { + const errorMessage = error?.response?.data?.message; + + if (errorMessage === 'Account not active, please verify your email') { + toast.error('Akun belum aktif, silakan verifikasi email Anda'); + openVerifyModal(data.email); + } else { + toast.error(errorMessage ?? 'Terjadi Kesalahan yang tidak diketahui'); + clearSession(); + } + }, + }); + }); return { form, onSubmit, + isLoading: isPending, + showVerifyModal, + verifyForm, + onVerifySubmit, + closeVerifyModal, + isVerifying, + emailToVerify, }; }; diff --git a/apps/gacha/src/app/_hooks/use-register.ts b/apps/gacha/src/app/_hooks/use-register.ts index d7c5531..0654304 100644 --- a/apps/gacha/src/app/_hooks/use-register.ts +++ b/apps/gacha/src/app/_hooks/use-register.ts @@ -6,9 +6,20 @@ import { } from '@imphnen-frontend-service/service'; import { zodResolver } from '@hookform/resolvers/zod'; import { toast } from 'sonner'; +import { useVerifyEmail } from './use-verify-email'; export const useRegister = () => { const postRegister = usePostRegister(); + const { + verifyForm, + onVerifySubmit, + showVerifyModal, + openVerifyModal, + closeVerifyModal, + isVerifying, + emailToVerify, + } = useVerifyEmail(); + const form = useForm({ resolver: zodResolver(authRegisterSchema), mode: 'all', @@ -23,13 +34,23 @@ export const useRegister = () => { const onSubmit = form.handleSubmit((data) => { postRegister.mutate(data, { - onSuccess: () => toast.success('Registrasi sukses'), - onError: (error) => toast.error(error.message), + onSuccess: () => { + toast.success('Registrasi sukses. Silakan verifikasi email Anda.'); + openVerifyModal(data.email); + }, + onError: (error) => toast.error(error.message || 'Registrasi gagal'), }); }); return { form, + verifyForm, onSubmit, + onVerifySubmit, + showVerifyModal, + closeVerifyModal, + isVerifying, + isRegistering: postRegister.isPending, + registeredEmail: emailToVerify, }; }; diff --git a/apps/gacha/src/app/_hooks/use-verify-email.ts b/apps/gacha/src/app/_hooks/use-verify-email.ts new file mode 100644 index 0000000..7591be3 --- /dev/null +++ b/apps/gacha/src/app/_hooks/use-verify-email.ts @@ -0,0 +1,62 @@ +import { useForm } from 'react-hook-form'; +import { + TVerifyEmailRequest, + usePostVerifyEmail, +} from '@imphnen-frontend-service/service'; +import { toast } from 'sonner'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export const useVerifyEmail = () => { + const [showVerifyModal, setShowVerifyModal] = useState(false); + const [emailToVerify, setEmailToVerify] = useState(''); + const navigate = useNavigate(); + + const postVerifyEmail = usePostVerifyEmail(); + + const verifyForm = useForm<{ otp: string }>({ + mode: 'all', + defaultValues: { + otp: '', + }, + }); + + const onVerifySubmit = verifyForm.handleSubmit((data) => { + const otpNumber = data.otp; + + const verifyData: TVerifyEmailRequest = { + email: emailToVerify, + otp: otpNumber, + }; + + postVerifyEmail.mutate(verifyData, { + onSuccess: () => { + toast.success('Verifikasi email sukses'); + + navigate(0); + }, + onError: (error) => + toast.error(error.message || 'Verifikasi email gagal'), + }); + }); + + const openVerifyModal = (email: string) => { + setEmailToVerify(email); + setShowVerifyModal(true); + }; + + const closeVerifyModal = () => { + setShowVerifyModal(false); + verifyForm.reset(); + }; + + return { + verifyForm, + onVerifySubmit, + showVerifyModal, + openVerifyModal, + closeVerifyModal, + isVerifying: postVerifyEmail.isPending, + emailToVerify, + }; +}; diff --git a/apps/gacha/src/app/page.tsx b/apps/gacha/src/app/page.tsx index c9cf71d..076a991 100644 --- a/apps/gacha/src/app/page.tsx +++ b/apps/gacha/src/app/page.tsx @@ -66,11 +66,15 @@ export const Components: FC = (): ReactElement => {
Merch 1 { ~ 175k ~
-
+
{ alt="Merch 3" /> Merch 1 - + diff --git a/apps/gacha/src/middleware.ts b/apps/gacha/src/middleware.ts index 3d4d24a..9eb0a9d 100644 --- a/apps/gacha/src/middleware.ts +++ b/apps/gacha/src/middleware.ts @@ -1,109 +1,15 @@ -import { - PERMISSIONS, - SessionToken, - SessionUser, -} from '@imphnen-frontend-service/utils'; -import { LoaderFunctionArgs, redirect } from 'react-router'; +// import { SessionToken, SessionUser } from '@imphnen-frontend-service/utils'; +import { LoaderFunctionArgs } from 'react-router'; -const mappingPublicRoutes = [ - '/', - '/auth/login', - '/auth/forgot', - '/auth/new-password', -]; +// const mappingPublicRoutes = ['/']; -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'); -// }; +// const mappingRoutePermissions = [ +// { +// path: '/', +// permissions: [], +// }, +// ]; 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 (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/api/auth/index.ts b/libs/service/src/api/auth/index.ts index efec499..7f1024c 100644 --- a/libs/service/src/api/auth/index.ts +++ b/libs/service/src/api/auth/index.ts @@ -35,7 +35,7 @@ export const postVerifyEmail = async ( const { data } = await api({ method: 'POST', url: '/auth/verify-email', - data: {otp: parseInt(payload.otp), email: payload.email}, + data: { otp: parseInt(payload.otp), email: payload.email }, }); return data; }; diff --git a/libs/ui/src/organisms/navbar/navbar.tsx b/libs/ui/src/organisms/navbar/navbar.tsx index 00a0e24..91a1c94 100644 --- a/libs/ui/src/organisms/navbar/navbar.tsx +++ b/libs/ui/src/organisms/navbar/navbar.tsx @@ -29,7 +29,7 @@ export const Navbar: FC = (): ReactElement => { @@ -38,7 +38,7 @@ export const Navbar: FC = (): ReactElement => { @@ -49,13 +49,17 @@ export const Navbar: FC = (): ReactElement => { ) : (
  • - {session.user?.fullname} -
    + {session?.user?.fullname} + +
    +
  • )} @@ -97,7 +101,19 @@ export const Navbar: FC = (): ReactElement => { ) : ( -
  • {session.user?.fullname}
  • + <> +
  • + {session?.user?.fullname} +
  • +
  • +
    + Logout +
    +
  • + )}
    diff --git a/libs/utils/src/hooks/use-auth-store.ts b/libs/utils/src/hooks/use-auth-store.ts index b5d0e39..7697a5c 100644 --- a/libs/utils/src/hooks/use-auth-store.ts +++ b/libs/utils/src/hooks/use-auth-store.ts @@ -25,7 +25,7 @@ export const useAuthStore = create((set) => { return { isLoading: false, - session: isAuthenticated ? { ...session, ...user } : undefined, + session: isAuthenticated ? { token: session.token, user } : undefined, status: isAuthenticated ? ESessionStatus.Authenticated : ESessionStatus.Unauthenticated, diff --git a/libs/utils/src/hooks/use-session.ts b/libs/utils/src/hooks/use-session.ts index ac47c71..053e6a5 100644 --- a/libs/utils/src/hooks/use-session.ts +++ b/libs/utils/src/hooks/use-session.ts @@ -6,7 +6,10 @@ import { useNavigate } from 'react-router'; export const useSession = () => { const navigate = useNavigate(); const { mutate, isPending } = usePostLogin(); - const { setLoading, setSession, clearSession, session } = useAuthStore(); + const { setLoading, setSession, clearSession, session, status } = + useAuthStore(); + const isAuthenticated = status === 'authenticated'; + const signIn = (payload: TLoginRequest) => { setLoading(true); mutate(payload, { @@ -33,5 +36,6 @@ export const useSession = () => { signIn, signOut, isLoading: isPending, + isAuthenticated, }; };