diff --git a/src/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpAuthUsecase.ts b/src/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpAuthUsecase.ts index 13824eb..33f1a36 100644 --- a/src/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpAuthUsecase.ts +++ b/src/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpAuthUsecase.ts @@ -26,11 +26,10 @@ export default class OtpAuthUsecase implements IOtpAuthUsecase { if (!isOtpValid) throw new OtpAuthUsecaseException(); // call the repo const updatedAdminModal = await this.repository.execute(data); - return updatedAdminModal; } private isOtpValid(otp: string) { - return !!(otp.length < this.validLenghtOfOtp); + return !!(otp.length >= this.validLenghtOfOtp); } } diff --git a/src/business-logic/generic/admin-user/authentication/phonnumber-auth/infra/phonenumberAuthInfra.ts b/src/business-logic/generic/admin-user/authentication/phonnumber-auth/infra/phonenumberAuthInfra.ts index ce1f4e2..04d7c46 100644 --- a/src/business-logic/generic/admin-user/authentication/phonnumber-auth/infra/phonenumberAuthInfra.ts +++ b/src/business-logic/generic/admin-user/authentication/phonnumber-auth/infra/phonenumberAuthInfra.ts @@ -6,10 +6,10 @@ export interface IPhonenumberAuthInfra { wrongPhoneNumberMessage: string; } -const phonenumberAuthInfra = async ( - httpHandler: RepoHttpHandler, - wrongPhoneNumberMessage: string, -): Promise => { +const phonenumberAuthInfra = ({ + httpHandler, + wrongPhoneNumberMessage, +}: IPhonenumberAuthInfra): PhonenumberAuthUsecase => { // prepare repo const repository = new PhoneNumberAuthRepo(httpHandler); // prepare usecase diff --git a/src/business-logic/generic/admin-user/common/data/model/adminUserModel.ts b/src/business-logic/generic/admin-user/common/data/model/adminUserModel.ts index 866bc32..575343b 100644 --- a/src/business-logic/generic/admin-user/common/data/model/adminUserModel.ts +++ b/src/business-logic/generic/admin-user/common/data/model/adminUserModel.ts @@ -1,7 +1,7 @@ import AdminUser from '../../entity/adminUserEntity'; export default class AdminUserModel { - private adminUserData: AdminUser; + adminUserData: AdminUser; constructor(adminUserData: AdminUser) { this.adminUserData = adminUserData; diff --git a/src/driven/adapters/auth-admin-login/authAdminLogin.ts b/src/driven/adapters/auth-admin-login/authAdminLogin.ts index e18f8d3..195a3a3 100644 --- a/src/driven/adapters/auth-admin-login/authAdminLogin.ts +++ b/src/driven/adapters/auth-admin-login/authAdminLogin.ts @@ -14,6 +14,7 @@ const authAdminLogin = (): IOtpAuthDrivenPort => { headers: { 'Content-Type': 'application/json', }, + method: 'POST', data, }; return httpProvider.request(options); diff --git a/src/driven/adapters/auth-admin-phonenumber/authAdminPhoneNumberDriven.ts b/src/driven/adapters/auth-admin-phonenumber/authAdminPhoneNumberDriven.ts index b3cf36c..11b0e09 100644 --- a/src/driven/adapters/auth-admin-phonenumber/authAdminPhoneNumberDriven.ts +++ b/src/driven/adapters/auth-admin-phonenumber/authAdminPhoneNumberDriven.ts @@ -17,6 +17,7 @@ const authAdminPhoneNumberDriven = (): IPhonenumberAuthPort => { headers: { 'Content-Type': 'application/json', }, + method: 'POST', data, }; return httpProvider.request(options); diff --git a/src/driven/boundaries/storage-boundary/index.ts b/src/driven/boundaries/storage-boundary/index.ts new file mode 100644 index 0000000..19578bc --- /dev/null +++ b/src/driven/boundaries/storage-boundary/index.ts @@ -0,0 +1,3 @@ +import StorageService from './storageService'; + +export default StorageService; diff --git a/src/driven/boundaries/storage-boundary/localStorageProvider.ts b/src/driven/boundaries/storage-boundary/localStorageProvider.ts new file mode 100644 index 0000000..96a90df --- /dev/null +++ b/src/driven/boundaries/storage-boundary/localStorageProvider.ts @@ -0,0 +1,18 @@ +import StorageProvider from './storageProvider'; + +export default class LocalStorageProvider implements StorageProvider { + setData(key: string, value: ValueType): void { + const jsonValue = JSON.stringify(value); + localStorage.setItem(key, jsonValue); + } + + getData(key: string): ValueType | null { + const jsonValue = localStorage.getItem(key); + const value: ValueType = jsonValue && JSON.parse(jsonValue); + return value; + } + + deleteData(key: string): void { + localStorage.removeItem(key); + } +} diff --git a/src/driven/boundaries/storage-boundary/storageProvider.ts b/src/driven/boundaries/storage-boundary/storageProvider.ts new file mode 100644 index 0000000..26b5cd1 --- /dev/null +++ b/src/driven/boundaries/storage-boundary/storageProvider.ts @@ -0,0 +1,7 @@ +export default abstract class StorageProvider { + abstract setData(key: string, value: ValueType): void; + + abstract getData(key: string): ValueType | null; + + abstract deleteData(key: string): void; +} diff --git a/src/driven/boundaries/storage-boundary/storageService.ts b/src/driven/boundaries/storage-boundary/storageService.ts new file mode 100644 index 0000000..7e73cf7 --- /dev/null +++ b/src/driven/boundaries/storage-boundary/storageService.ts @@ -0,0 +1,27 @@ +import LocalStorageProvider from './localStorageProvider'; +import StorageProvider from './storageProvider'; + +export default class StorageService implements StorageProvider { + private provider: StorageProvider; + + constructor(provider: StorageProvider) { + this.provider = provider; + } + + static localStorage(): StorageService { + const localStorageService = new StorageService(new LocalStorageProvider()); + return localStorageService; + } + + setData(key: string, value: ValueType): void { + return this.provider.setData(key, value); + } + + getData(key: string): ValueType | null { + return this.provider.getData(key); + } + + deleteData(key: string): void { + this.provider.deleteData(key); + } +} diff --git a/src/driven/utils/configs/appConfig.ts b/src/driven/utils/configs/appConfig.ts index a05d177..d6d78fa 100644 --- a/src/driven/utils/configs/appConfig.ts +++ b/src/driven/utils/configs/appConfig.ts @@ -2,7 +2,9 @@ import { icons } from '../constants/assertUrls'; import { ENVs } from '../constants/envs'; import { staticMessages } from '../constants/staticMessages'; -export const appConfig = {}; +export const appConfig = { + adminUserStorageKey: 'adminUser', +}; export const routes = { usersList: '/', diff --git a/src/driven/utils/constants/staticMessages.ts b/src/driven/utils/constants/staticMessages.ts index 20dd09e..c21228c 100644 --- a/src/driven/utils/constants/staticMessages.ts +++ b/src/driven/utils/constants/staticMessages.ts @@ -3,6 +3,7 @@ export const staticMessages = { errors: { input: 'please fill all inputs correctly', phonenumber: 'please fill the valid number', + otp: 'please fill the otp fields correctly', }, users: 'Users', submit: 'Submit', diff --git a/src/driven/utils/helpers/contexts/userContext.tsx b/src/driven/utils/helpers/contexts/userContext.tsx new file mode 100644 index 0000000..ed3f92e --- /dev/null +++ b/src/driven/utils/helpers/contexts/userContext.tsx @@ -0,0 +1,17 @@ +/* eslint-disable react/jsx-no-constructed-context-values */ +import React, { useState } from 'react'; +import AdminUserModel from '~/business-logic/generic/admin-user/common/data/model/adminUserModel'; + +interface IUserContext { + user: AdminUserModel | null; + setUser: React.Dispatch> | null; +} +export const UserContext = React.createContext({ user: null, setUser: null }); + +export function UserProvider({ children }: React.PropsWithChildren) { + const [user, setUser] = useState(null); + + return {children}; +} + +export const useUser = () => React.useContext(UserContext); diff --git a/src/driving/application/generic/authentication/infra/Authentication.tsx b/src/driving/application/generic/authentication/infra/Authentication.tsx index 2414a2f..a2f5608 100644 --- a/src/driving/application/generic/authentication/infra/Authentication.tsx +++ b/src/driving/application/generic/authentication/infra/Authentication.tsx @@ -1,6 +1,16 @@ import React from 'react'; +import authAdminPhoneNumberDriven from '~/driven/adapters/auth-admin-phonenumber/authAdminPhoneNumberDriven'; +import phonenumberAuthInfra from '~/business-logic/generic/admin-user/authentication/phonnumber-auth'; +import authAdminLogin from '~/driven/adapters/auth-admin-login/authAdminLogin'; +import otpAuthInfra from '~/business-logic/generic/admin-user/authentication/otp-auth'; import AuthenticationView from '../view/AuthenticationView'; export default function Authentication() { - return ; + const authPhonenumberDriven = authAdminPhoneNumberDriven(); + const authPhoneLogic = phonenumberAuthInfra(authPhonenumberDriven); + + const authLoginDriven = authAdminLogin(); + const otpAuthLogic = otpAuthInfra(authLoginDriven.httpHandler); + + return ; } diff --git a/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx b/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx index 3d2d947..0f7b7ec 100644 --- a/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx +++ b/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx @@ -20,7 +20,7 @@ export default function OtpCodeView(props: IOtpCodeView) { tabIndex={i + 1} ref={(el: HTMLInputElement) => (otpChar.current[i] = el)} key={`otp_char_${i}`} - className='font-bold text-base inline-block w-5 md:w-10 bg-transparent text-center focus:outline-none' + className='font-bold text-base inline-block w-5 md:w-6 lg:w-8 xl:w-10 bg-transparent text-center focus:outline-none' maxLength={1} defaultValue='_' placeholder='_' diff --git a/src/driving/application/generic/authentication/view/AuthenticationView.tsx b/src/driving/application/generic/authentication/view/AuthenticationView.tsx index 56ad942..2b02379 100644 --- a/src/driving/application/generic/authentication/view/AuthenticationView.tsx +++ b/src/driving/application/generic/authentication/view/AuthenticationView.tsx @@ -2,11 +2,30 @@ import React, { useRef, useState } from 'react'; import PrimaryButton from '~/driven/utils/components/buttons/primary-button/PrimaryButton'; import { icons } from '~/driven/utils/constants/assertUrls'; import { staticMessages } from '~/driven/utils/constants/staticMessages'; +import OtpAuthUsecase from '~/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpAuthUsecase'; +import PhonenumberAuthUsecase from '~/business-logic/generic/admin-user/authentication/phonnumber-auth/usecase/phonenumberAuthUsecase'; +import StorageService from '~/driven/boundaries/storage-boundary'; +import { appConfig } from '~/driven/utils/configs/appConfig'; +import Notification from '~/driven/utils/components/Notification/Notification'; +import OtpAuthUsecaseException from '~/business-logic/generic/admin-user/authentication/otp-auth/usecase/otpException'; +import { AxiosError } from 'axios'; +import { useUser } from '~/driven/utils/helpers/contexts/userContext'; import OtpCode from '../otp-code-inputs'; import PhoneNumberAuth from './PhoneNumberAuth'; -export default function AuthenticationView() { +interface IAuthenticationView { + authPhone: PhonenumberAuthUsecase; + otpAuth: OtpAuthUsecase; +} + +/** + * @todo should seperate logic and ui logic from the view + */ +export default function AuthenticationView(props: IAuthenticationView) { + const { authPhone, otpAuth } = props; const [phoneNumberValue, setPhoneNumberValue] = useState(''); + const [error, setError] = useState(''); + const { setUser } = useUser(); const otpChar = useRef([]); const statesName = { phonenumber: , @@ -14,12 +33,37 @@ export default function AuthenticationView() { }; const [authState, setAuthState] = useState('phonenumber'); - const submitForm = (e: React.FormEvent) => { + const submitForm = async (e: React.FormEvent) => { e.preventDefault(); - setAuthState('otp'); + try { + if (authState === 'phonenumber') { + await authPhone.execute({ phonenumber: phoneNumberValue }); + setAuthState('otp'); + } + if (authState === 'otp') { + const otp = otpChar.current + .map((inputItem) => { + return inputItem.value; + }) + .join(''); + const userModel = await otpAuth.execute({ otp, phonenumber: phoneNumberValue }); + const storage = StorageService.localStorage(); + storage.setData(appConfig.adminUserStorageKey, userModel); + if (setUser) setUser(userModel); + } + } catch (errorException) { + if (errorException instanceof OtpAuthUsecaseException) { + setError(staticMessages.global.errors.otp); + } else if (errorException instanceof AxiosError) { + setError(errorException.response?.data?.description); + } else if (errorException instanceof Error) { + setError(errorException.message); + } + } }; return (
+ {Boolean(error) && setError('')} />}
- - - ); + const data = useUser(); + const { setUser } = data; + + useEffect(() => { + const storage = StorageService.localStorage(); + const currentUser = storage.getData(appConfig.adminUserStorageKey); + if (currentUser && currentUser.adminUserData.accessToken && setUser) setUser(currentUser); + }, []); + return ; } export default App; diff --git a/src/driving/main/Router/Router.tsx b/src/driving/main/Router/Router.tsx index edfd23a..d674476 100644 --- a/src/driving/main/Router/Router.tsx +++ b/src/driving/main/Router/Router.tsx @@ -4,6 +4,7 @@ import { routes } from '~/driven/utils/configs/appConfig'; import CreateUser from '../pages/CreateUser'; import MainPageLayout from '../pages/layouts/MainPageLayout'; import AuthenticationPage from '../pages/Authentication'; +import UserLoginLayout from '../pages/layouts/UserLoginLayout'; export default function Router() { const location = useLocation(); @@ -14,7 +15,9 @@ export default function Router() { } /> } /> - } /> + }> + } /> + } /> ); diff --git a/src/driving/main/index.tsx b/src/driving/main/index.tsx index fd17d55..240b5cf 100644 --- a/src/driving/main/index.tsx +++ b/src/driving/main/index.tsx @@ -1,4 +1,12 @@ import ReactDOM from 'react-dom/client'; +import { UserProvider } from '~/driven/utils/helpers/contexts/userContext'; +import { BrowserRouter as RouterWrapper } from 'react-router-dom'; import App from './App'; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + , +); diff --git a/src/driving/main/pages/layouts/MainPageLayout.tsx b/src/driving/main/pages/layouts/MainPageLayout.tsx index bac325a..86da94c 100644 --- a/src/driving/main/pages/layouts/MainPageLayout.tsx +++ b/src/driving/main/pages/layouts/MainPageLayout.tsx @@ -1,8 +1,24 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; +import AdminUserModel from '~/business-logic/generic/admin-user/common/data/model/adminUserModel'; +import StorageService from '~/driven/boundaries/storage-boundary'; +import { appConfig, routes } from '~/driven/utils/configs/appConfig'; +import { useUser } from '~/driven/utils/helpers/contexts/userContext'; import Sidebar from '~/driving/application/support/sidebar'; +/** + * @todo should move to the logic and seperate the ui from the main logics + */ export default function MainPageLayout() { + const { user, setUser } = useUser(); + + useEffect(() => { + const storage = StorageService.localStorage(); + const currentUser = storage.getData(appConfig.adminUserStorageKey); + if (!currentUser && setUser) setUser(null); + }); + + if (!user || !user.adminUserData.accessToken) return ; return (
diff --git a/src/driving/main/pages/layouts/UserLoginLayout.tsx b/src/driving/main/pages/layouts/UserLoginLayout.tsx new file mode 100644 index 0000000..b83e787 --- /dev/null +++ b/src/driving/main/pages/layouts/UserLoginLayout.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Navigate, Outlet } from 'react-router-dom'; +import { routes } from '~/driven/utils/configs/appConfig'; +import { useUser } from '~/driven/utils/helpers/contexts/userContext'; + +export default function UserLoginLayout() { + const { user } = useUser(); + + if (user) return ; + return ; +}