From e8c02ab85320fb0ba1b1e890021ae4a7b7361cdd Mon Sep 17 00:00:00 2001 From: mengstabketemaw Date: Sat, 11 Jul 2026 10:09:18 +0300 Subject: [PATCH] feat: fayda integration paritial --- apps/backoffice/vite.config.mts | 4 +- apps/portal/src/app/router.tsx | 8 +- apps/portal/vite.config.mts | 4 +- libs/auth/src/index.ts | 2 + libs/auth/src/lib/pages/FaydaCallbackPage.tsx | 110 ++++++++++++++ libs/auth/src/lib/pages/SetPasswordPage.tsx | 139 ++++++++++++++++++ libs/auth/src/lib/pages/SignupPage.tsx | 33 +++++ 7 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 libs/auth/src/lib/pages/FaydaCallbackPage.tsx create mode 100644 libs/auth/src/lib/pages/SetPasswordPage.tsx diff --git a/apps/backoffice/vite.config.mts b/apps/backoffice/vite.config.mts index daa242e8b..914421cd4 100644 --- a/apps/backoffice/vite.config.mts +++ b/apps/backoffice/vite.config.mts @@ -6,10 +6,10 @@ export default defineConfig({ root: __dirname, cacheDir: '../../node_modules/.vite/apps/backoffice', server: { - port: 4201, + port: Number(process.env.PORT) || 4201, host: 'localhost', }, - preview: { port: 4201, host: 'localhost' }, + preview: { port: Number(process.env.PORT) || 4201, host: 'localhost' }, plugins: [react(), nxViteTsPaths()], resolve: { dedupe: ['react', 'react-dom', 'react-router-dom', '@tanstack/react-query'], diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index 95a4b7be3..424f08514 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -7,7 +7,7 @@ import { ProfileGuard } from './components/ProfileGuard'; import { SmartDashboard } from './components/SmartDashboard'; // Auth (standalone pages, no portal chrome) -import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth'; +import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage, FaydaCallbackPage, SetPasswordPage } from '@ema-platform/auth'; // Profile setup import { ProfileSetupPage } from './features/profile-setup/pages/ProfileSetupPage'; @@ -51,6 +51,12 @@ export const router = createBrowserRouter([ // Public auth pages { path: '/forgot-password', element: }, + // Fayda / National ID callback + { path: '/callback', element: }, + + // Set password (after Fayda signup) + { path: '/set-password', element: }, + // Protected auth pages { element: , diff --git a/apps/portal/vite.config.mts b/apps/portal/vite.config.mts index 66e5b1977..e8022c6b9 100644 --- a/apps/portal/vite.config.mts +++ b/apps/portal/vite.config.mts @@ -5,8 +5,8 @@ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; export default defineConfig({ root: __dirname, cacheDir: '../../node_modules/.vite/apps/portal', - server: { port: 4200, host: 'localhost' }, - preview: { port: 4200, host: 'localhost' }, + server: { port: Number(process.env.PORT) || 3000, host: 'localhost' }, + preview: { port: Number(process.env.PORT) || 3000, host: 'localhost' }, plugins: [react(), nxViteTsPaths()], resolve: { dedupe: ['react', 'react-dom', 'react-router-dom', '@tanstack/react-query'], diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index 1b729dbd7..a72b82e03 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -4,6 +4,8 @@ export { LoginPage } from './lib/pages/LoginPage'; export { SignupPage } from './lib/pages/SignupPage'; export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage'; export { OTPVerificationPage } from './lib/pages/OTPVerificationPage'; +export { FaydaCallbackPage } from './lib/pages/FaydaCallbackPage'; +export { SetPasswordPage } from './lib/pages/SetPasswordPage'; export { authReducer, loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } from './lib/store/auth.slice'; export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice'; export { authStorage } from './lib/utils/auth-storage'; diff --git a/libs/auth/src/lib/pages/FaydaCallbackPage.tsx b/libs/auth/src/lib/pages/FaydaCallbackPage.tsx new file mode 100644 index 000000000..16d2a9c26 --- /dev/null +++ b/libs/auth/src/lib/pages/FaydaCallbackPage.tsx @@ -0,0 +1,110 @@ +import { useEffect, useRef, useState } from 'react'; +import { Alert, Center, Loader, Stack, Text, Title } from '@mantine/core'; +import { IconAlertCircle, IconShieldCheck } from '@tabler/icons-react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useApiMutation } from '@ema-platform/api'; +import { notify } from '@ema-platform/ui'; +import { AuthShell } from '../components/AuthShell'; + +interface FaydaUserInfo { + name: string; + email: string; + sub: string; + birthDate: string; + gender: string; + picture: string; + phone: string; + address: string; +} + +export function FaydaCallbackPage() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const calledRef = useRef(false); + const [error, setError] = useState(null); + + const [getUserInfoTrigger] = useApiMutation(); + + useEffect(() => { + if (calledRef.current) return; + calledRef.current = true; + + const code = searchParams.get('code'); + const state = searchParams.get('state'); + + if (!code || !state) { + const msg = 'Invalid response from National ID service. Missing code or state.'; + setError(msg); + notify.error(msg); + return; + } + + const authId = sessionStorage.getItem('fayda_authId'); + sessionStorage.removeItem('fayda_authId'); + + if (!authId) { + const msg = 'Session expired. Please try signing up again.'; + setError(msg); + notify.error(msg); + return; + } + + const handleCallback = async () => { + try { + const userInfo = await getUserInfoTrigger({ + url: '/utilities/get-national-id-user', + method: 'POST', + body: { state, code, authId }, + }).unwrap(); + + sessionStorage.setItem('fayda_userId', userInfo.sub); + sessionStorage.setItem('fayda_userInfo', JSON.stringify(userInfo)); + + notify.success('National ID verified successfully'); + navigate('/set-password', { replace: true }); + } catch (err: unknown) { + const msg = + (err as { data?: { message?: string } })?.data?.message ?? + (err instanceof Error ? err.message : 'Failed to complete National ID verification'); + setError(msg); + notify.error(msg); + } + }; + + handleCallback(); + }, [searchParams, getUserInfoTrigger, navigate]); + + return ( + + + +
+ + {error ? 'Verification failed' : 'Verifying your identity'} + + + {error + ? error + : 'We are securely processing your National ID information. This should only take a moment.'} + +
+ + {!error && } + + {error && ( + <> + }> + {error} + + + Please try again or use the standard sign-up method instead. + + + )} +
+
+ ); +} \ No newline at end of file diff --git a/libs/auth/src/lib/pages/SetPasswordPage.tsx b/libs/auth/src/lib/pages/SetPasswordPage.tsx new file mode 100644 index 000000000..50681af11 --- /dev/null +++ b/libs/auth/src/lib/pages/SetPasswordPage.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { Alert, Button, PasswordInput, Stack, Text, ThemeIcon, Title } from '@mantine/core'; +import { IconArrowRight, IconLock, IconLockOpen } from '@tabler/icons-react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { useNavigate } from 'react-router-dom'; +import { useApiMutation } from '@ema-platform/api'; +import { notify } from '@ema-platform/ui'; +import { AuthShell } from '../components/AuthShell'; + +const schema = z + .object({ + newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }), + confirmPassword: z.string().min(8, { message: 'Confirm your password' }), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); + +type FormValues = z.infer; + +export function SetPasswordPage() { + const navigate = useNavigate(); + const [serverError, setServerError] = useState(null); + const [setPasswordTrigger, { isLoading }] = useApiMutation(); + + const userId = sessionStorage.getItem('fayda_userId'); + const userInfoRaw = sessionStorage.getItem('fayda_userInfo'); + + let userName = 'your account'; + if (userInfoRaw) { + try { + const info = JSON.parse(userInfoRaw); + userName = info.name || 'your account'; + } catch { + // ignore + } + } + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + }); + + const onSubmit = async (values: FormValues) => { + if (!userId) { + notify.error('Session expired. Please sign up again.'); + navigate('/signup', { replace: true }); + return; + } + + try { + await setPasswordTrigger({ + url: '/v1/auth/set-fayda-password', + method: 'PATCH', + body: { + userId, + newPassword: values.newPassword, + confirmPassword: values.confirmPassword, + }, + }).unwrap(); + + sessionStorage.removeItem('fayda_userId'); + sessionStorage.removeItem('fayda_userInfo'); + + notify.success('Password set successfully'); + navigate('/login', { replace: true }); + } catch (err: unknown) { + const msg = + (err as { data?: { message?: string } })?.data?.message ?? + (err instanceof Error ? err.message : 'Something went wrong'); + setServerError(msg); + notify.error(msg); + } + }; + + return ( + + + + + + +
+ + Set your password + + + Welcome, {userName}. Your National ID has been verified successfully. + Choose a password to complete your account setup. + +
+ + {serverError && ( + setServerError(null)}> + {serverError} + + )} + +
+ + } + error={errors.newPassword?.message} + {...register('newPassword')} + /> + } + error={errors.confirmPassword?.message} + {...register('confirmPassword')} + /> + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/libs/auth/src/lib/pages/SignupPage.tsx b/libs/auth/src/lib/pages/SignupPage.tsx index 970c285ea..195e64fa9 100644 --- a/libs/auth/src/lib/pages/SignupPage.tsx +++ b/libs/auth/src/lib/pages/SignupPage.tsx @@ -4,6 +4,7 @@ import { Anchor, Button, Checkbox, + Divider, Group, PasswordInput, SimpleGrid, @@ -16,6 +17,7 @@ import { IconArrowRight, IconAt, IconDeviceMobile, + IconId, IconLock, IconMail, IconUser, @@ -77,6 +79,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) { isPhoneNumberVerified: boolean; }>(); const [meTrigger] = useApiMutation(); + const [faydaInitTrigger] = useApiMutation<{ url: string; authId: string }>(); const { register, @@ -87,6 +90,23 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) { defaultValues: { userType: 'individual' }, }); + const handleFaydaSignup = async () => { + try { + const data = await faydaInitTrigger({ + url: '/utilities/init-national-id-auth', + method: 'GET', + }).unwrap(); + sessionStorage.setItem('fayda_authId', data.authId); + window.location.href = data.url; + } catch (err: unknown) { + const msg = + (err as { data?: { message?: string } })?.data?.message ?? + (err instanceof Error ? err.message : 'Failed to initiate National ID verification'); + setServerError(msg); + notify.error(msg); + } + }; + const onSubmit = async (values: FormValues) => { try { const payload: SignupPayload = { @@ -246,6 +266,19 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) { > Create account + + + +