diff --git a/apps/backoffice/public/assets/emaLogo.jpg b/apps/backoffice/public/assets/emaLogo.jpg new file mode 100644 index 000000000..92310f682 Binary files /dev/null and b/apps/backoffice/public/assets/emaLogo.jpg differ diff --git a/apps/backoffice/src/app/features/auth/api/auth-api.ts b/apps/backoffice/src/app/features/auth/api/auth-api.ts deleted file mode 100644 index 198865d69..000000000 --- a/apps/backoffice/src/app/features/auth/api/auth-api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { baseApi } from '@ema-platform/api'; -import type { LoginPayload } from '../types/auth.types'; - -interface LoginArgs { - email: string; - password: string; -} - -interface RefreshArgs { - refreshToken: string; -} - -const authApi = baseApi.injectEndpoints({ - endpoints: (builder) => ({ - login: builder.mutation({ - query: (body) => ({ url: '/auth/login', method: 'POST', body }), - }), - refresh: builder.mutation<{ token: string }, RefreshArgs>({ - query: (body) => ({ url: '/auth/refresh', method: 'POST', body }), - }), - logout: builder.mutation<{ message: string }, void>({ - query: () => ({ url: '/auth/logout', method: 'POST' }), - }), - }), - overrideExisting: false, -}); - -export const { useLoginMutation, useRefreshMutation, useLogoutMutation } = authApi; diff --git a/apps/backoffice/src/app/features/auth/components/LoginForm.tsx b/apps/backoffice/src/app/features/auth/components/LoginForm.tsx deleted file mode 100644 index 57e463101..000000000 --- a/apps/backoffice/src/app/features/auth/components/LoginForm.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { - TextInput, - PasswordInput, - Button, - Stack, - Title, - Paper, - Text, - Anchor, -} from '@mantine/core'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { useNavigate, Link } from 'react-router-dom'; -import { useLoginMutation } from '../api/auth-api'; -import { useAuth } from '../hooks/useAuth'; -import { notify } from '@ema-platform/ui'; - -const schema = z.object({ - email: z.string().email('Enter a valid email'), - password: z.string().min(6, 'Password must be at least 6 characters'), -}); - -type FormValues = z.infer; - -export function LoginForm() { - const navigate = useNavigate(); - const { login } = useAuth(); - const [loginMutate, { isLoading }] = useLoginMutation(); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(schema), - }); - - const onSubmit = async (values: FormValues) => { - try { - const result = await loginMutate(values).unwrap(); - login(result); - navigate('/dashboard'); - } catch { - notify.error('Invalid email or password'); - } - }; - - return ( - - - - Sign in - - Enter your credentials to access the backoffice - - -
- - - - - -
- - Don't have an account?{' '} - - Sign up - - -
-
- ); -} diff --git a/apps/backoffice/src/app/features/auth/hooks/useAuth.ts b/apps/backoffice/src/app/features/auth/hooks/useAuth.ts deleted file mode 100644 index b1a7924d2..000000000 --- a/apps/backoffice/src/app/features/auth/hooks/useAuth.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useAppDispatch, useAppSelector } from '../../../store/hooks'; -import { loginSuccess, logout as logoutAction, hydrateAuth } from '../store/auth.slice'; -import type { LoginPayload } from '../types/auth.types'; - -export function useAuth() { - const dispatch = useAppDispatch(); - const { user, token, isAuthenticated } = useAppSelector((s) => s.auth); - - return { - user, - token, - isAuthenticated, - login: (payload: LoginPayload) => dispatch(loginSuccess(payload)), - logout: () => dispatch(logoutAction()), - hydrate: () => dispatch(hydrateAuth()), - }; -} diff --git a/apps/backoffice/src/app/features/auth/pages/LoginPage.tsx b/apps/backoffice/src/app/features/auth/pages/LoginPage.tsx deleted file mode 100644 index 2ca197106..000000000 --- a/apps/backoffice/src/app/features/auth/pages/LoginPage.tsx +++ /dev/null @@ -1 +0,0 @@ -export { Login as LoginPage } from '@tria-plc/iamui-common'; diff --git a/apps/backoffice/src/app/features/auth/pages/SetPasswordPage.tsx b/apps/backoffice/src/app/features/auth/pages/SetPasswordPage.tsx deleted file mode 100644 index d8143fe06..000000000 --- a/apps/backoffice/src/app/features/auth/pages/SetPasswordPage.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SetPasswordPage } from '@tria-plc/iamui-common'; diff --git a/apps/backoffice/src/app/features/auth/pages/SignupPage.tsx b/apps/backoffice/src/app/features/auth/pages/SignupPage.tsx deleted file mode 100644 index 05ce9eae0..000000000 --- a/apps/backoffice/src/app/features/auth/pages/SignupPage.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { - Paper, - TextInput, - Button, - Stack, - Title, - Text, - Anchor, -} from '@mantine/core'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { useNavigate, Link } from 'react-router-dom'; -import { useAppDispatch, useAppSelector } from '../../../store/hooks'; -import { signupStart, signupSuccess, signupFailure } from '../store/signup.slice'; -import { hydrateAuth } from '../store/auth.slice'; -import { authStorage } from '../utils/auth-storage'; -import { notify } from '@ema-platform/ui'; - -const BASE_API_URL = - (import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ?? - 'http://localhost:3001/api'; - -const schema = z.object({ - email: z.string().email('Enter a valid email'), - username: z.string().min(3, 'Username must be at least 3 characters'), - phoneNumber: z.string().min(1, 'Phone number is required'), - userType: z.string().min(1, 'User type is required'), - name: z.string().min(1, 'Name is required'), -}); - -type FormValues = z.infer; - -interface SignupPayload { - email: string; - username: string; - phoneNumber: string; - userType: string; - name: { - am: string; - en: string; - }; -} - -export function SignupPage() { - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const { loading } = useAppSelector((s) => s.signup); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(schema), - }); - - const onSubmit = async (values: FormValues) => { - dispatch(signupStart()); - try { - const payload: SignupPayload = { - email: values.email, - username: values.username, - phoneNumber: values.phoneNumber, - userType: values.userType, - name: { - am: '', - en: values.name, - }, - }; - - const res = await fetch(`${BASE_API_URL}/auth/signup`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => null); - throw new Error(errorData?.message ?? 'Signup failed'); - } - - const data = (await res.json()) as { token: string; refreshToken: string }; - - authStorage.setToken(data.token); - authStorage.setRefreshToken(data.refreshToken); - dispatch(hydrateAuth()); - dispatch(signupSuccess()); - - navigate('/set-password', { state: { email: values.email } }); - } catch (err) { - const msg = err instanceof Error ? err.message : 'Something went wrong'; - dispatch(signupFailure(msg)); - notify.error(msg); - } - }; - - return ( - - - - Create an account - - Fill in your details to get started - - -
- - - - - - - - -
- - Already have an account?{' '} - - Sign in - - -
-
- ); -} diff --git a/apps/backoffice/src/app/features/auth/store/auth.slice.ts b/apps/backoffice/src/app/features/auth/store/auth.slice.ts deleted file mode 100644 index 5c9dbb9fb..000000000 --- a/apps/backoffice/src/app/features/auth/store/auth.slice.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import type { AuthState, LoginPayload } from '../types/auth.types'; -import { authStorage } from '../utils/auth-storage'; - -const initialState: AuthState = { - user: null, - token: null, - isAuthenticated: false, -}; - -const authSlice = createSlice({ - name: 'auth', - initialState, - reducers: { - loginSuccess(state, action: PayloadAction) { - state.user = action.payload.user; - state.token = action.payload.token; - state.isAuthenticated = true; - authStorage.setToken(action.payload.token); - authStorage.setRefreshToken(action.payload.refreshToken); - authStorage.setUser(action.payload.user); - }, - logout(state) { - state.user = null; - state.token = null; - state.isAuthenticated = false; - authStorage.clear(); - }, - hydrateAuth(state) { - const token = authStorage.getToken(); - const user = authStorage.getUser(); - if (token && user) { - state.token = token; - state.user = user; - state.isAuthenticated = true; - } - }, - }, -}); - -export const { loginSuccess, logout, hydrateAuth } = authSlice.actions; -export const authReducer = authSlice.reducer; diff --git a/apps/backoffice/src/app/features/auth/store/set-password.slice.ts b/apps/backoffice/src/app/features/auth/store/set-password.slice.ts deleted file mode 100644 index bdcf8d745..000000000 --- a/apps/backoffice/src/app/features/auth/store/set-password.slice.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; - -interface SetPasswordState { - loading: boolean; - success: boolean; - error: string | null; -} - -const initialState: SetPasswordState = { - loading: false, - success: false, - error: null, -}; - -const setPasswordSlice = createSlice({ - name: 'setPassword', - initialState, - reducers: { - setPasswordStart(state) { - state.loading = true; - state.error = null; - state.success = false; - }, - setPasswordSuccess(state) { - state.loading = false; - state.success = true; - }, - setPasswordFailure(state, action: PayloadAction) { - state.loading = false; - state.error = action.payload; - }, - resetSetPassword(state) { - state.loading = false; - state.success = false; - state.error = null; - }, - }, -}); - -export const { setPasswordStart, setPasswordSuccess, setPasswordFailure, resetSetPassword } = - setPasswordSlice.actions; -export const setPasswordReducer = setPasswordSlice.reducer; diff --git a/apps/backoffice/src/app/features/auth/store/signup.slice.ts b/apps/backoffice/src/app/features/auth/store/signup.slice.ts deleted file mode 100644 index 92a5f4294..000000000 --- a/apps/backoffice/src/app/features/auth/store/signup.slice.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; - -interface SignupState { - loading: boolean; - success: boolean; - error: string | null; -} - -const initialState: SignupState = { - loading: false, - success: false, - error: null, -}; - -const signupSlice = createSlice({ - name: 'signup', - initialState, - reducers: { - signupStart(state) { - state.loading = true; - state.error = null; - state.success = false; - }, - signupSuccess(state) { - state.loading = false; - state.success = true; - }, - signupFailure(state, action: PayloadAction) { - state.loading = false; - state.error = action.payload; - }, - resetSignup(state) { - state.loading = false; - state.success = false; - state.error = null; - }, - }, -}); - -export const { signupStart, signupSuccess, signupFailure, resetSignup } = signupSlice.actions; -export const signupReducer = signupSlice.reducer; diff --git a/apps/backoffice/src/app/features/auth/types/auth.types.ts b/apps/backoffice/src/app/features/auth/types/auth.types.ts deleted file mode 100644 index 2f45a66a3..000000000 --- a/apps/backoffice/src/app/features/auth/types/auth.types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface AuthUser { - id: string; - email: string; - username: string; - roles: string[]; - permissions: string[]; -} - -export interface AuthState { - user: AuthUser | null; - token: string | null; - isAuthenticated: boolean; -} - -export interface LoginPayload { - user: AuthUser; - token: string; - refreshToken: string; -} diff --git a/apps/backoffice/src/app/features/auth/utils/auth-storage.ts b/apps/backoffice/src/app/features/auth/utils/auth-storage.ts deleted file mode 100644 index dc4aa3010..000000000 --- a/apps/backoffice/src/app/features/auth/utils/auth-storage.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AuthUser } from '../types/auth.types'; - -const KEYS = { - token: 'ema-backoffice-auth-token', - refreshToken: 'ema-backoffice-refresh-token', - user: 'ema-backoffice-auth-user', -} as const; - -const COOKIE_ATTRS = 'path=/;max-age=86400'; - -function setCookie(name: string, value: string) { - document.cookie = `${name}=${encodeURIComponent(value)};${COOKIE_ATTRS}`; -} - -function removeCookie(name: string) { - document.cookie = `${name}=;path=/;max-age=0`; -} - -export const authStorage = { - getToken: () => localStorage.getItem(KEYS.token) ?? undefined, - setToken: (token: string) => { - localStorage.setItem(KEYS.token, token); - setCookie('auth-token', token); - }, - removeToken: () => { - localStorage.removeItem(KEYS.token); - removeCookie('auth-token'); - }, - - getRefreshToken: () => localStorage.getItem(KEYS.refreshToken) ?? undefined, - setRefreshToken: (token: string) => { - localStorage.setItem(KEYS.refreshToken, token); - setCookie('refresh-token', token); - }, - - getUser: (): AuthUser | null => { - const raw = localStorage.getItem(KEYS.user); - try { - return raw ? (JSON.parse(raw) as AuthUser) : null; - } catch { - return null; - } - }, - setUser: (user: AuthUser) => localStorage.setItem(KEYS.user, JSON.stringify(user)), - - clear: () => { - Object.values(KEYS).forEach((k) => localStorage.removeItem(k)); - removeCookie('auth-token'); - removeCookie('refresh-token'); - }, -}; diff --git a/apps/backoffice/src/app/store/index.ts b/apps/backoffice/src/app/store/index.ts index bed66a931..4a2291664 100644 --- a/apps/backoffice/src/app/store/index.ts +++ b/apps/backoffice/src/app/store/index.ts @@ -1,14 +1,8 @@ import { configureStore } from '@reduxjs/toolkit'; import { baseApi } from '@ema-platform/api'; -import { authReducer } from '../features/auth/store/auth.slice'; -import { signupReducer } from '../features/auth/store/signup.slice'; -import { setPasswordReducer } from '../features/auth/store/set-password.slice'; export const store = configureStore({ reducer: { - auth: authReducer, - signup: signupReducer, - setPassword: setPasswordReducer, [baseApi.reducerPath]: baseApi.reducer, }, middleware: (getDefaultMiddleware) => diff --git a/apps/backoffice/src/main.tsx b/apps/backoffice/src/main.tsx index 7d9ec4e29..1d967ab5b 100644 --- a/apps/backoffice/src/main.tsx +++ b/apps/backoffice/src/main.tsx @@ -7,6 +7,42 @@ import '@tria-plc/iamui-common/styles.css'; import './styles.css'; import { App } from './app/app'; +window.__USER_MANAGEMENT_BRANDING__ = { + appName: 'Ethiopian Maritime Licence', + organizationName: 'Ethiopian Maritime Authority', + logoSrc: '/assets/emaLogo.jpg', + logoAlt: 'EMA Logo', + homePath: '/', + moduleBasePath: '/user-management', + backToAppPath: '/dashboard', + backToAppLabel: 'Back to dashboard', + cssVariables: { + '--primary': '#2563eb', + '--primary-foreground': 'oklch(0.99 0 0)', + '--secondary': '#1d4ed8', + '--secondary-foreground': 'oklch(0.99 0 0)', + '--accent': '#60a5fa', + '--accent-foreground': 'oklch(0.99 0 0)', + '--ring': '#2563eb', + '--sidebar': 'oklch(0.21 0.04 265)', + '--sidebar-foreground': 'oklch(0.96 0.01 255)', + '--sidebar-primary': '#3b82f6', + '--sidebar-primary-foreground': 'oklch(0.99 0 0)', + '--sidebar-accent': '#60a5fa', + '--sidebar-accent-foreground': 'oklch(0.96 0.01 255)', + '--sidebar-border': 'oklch(0.3 0.04 265)', + '--sidebar-ring': '#3b82f6', + '--brand-shell-bg': '#eff6ff', + '--brand-sidebar-bg': '#0f172a', + '--brand-sidebar-border': '#1e293b', + '--brand-sidebar-muted': '#94a3b8', + '--brand-primary-solid': '#2563eb', + '--brand-primary-hover': '#1d4ed8', + '--brand-hero-from': '#2563eb', + '--brand-hero-to': '#1e40af', + }, +}; + const root = document.getElementById('root'); if (!root) throw new Error('Root element not found'); diff --git a/libs/api/src/lib/query-and-mutation/index.ts b/libs/api/src/lib/query-and-mutation/index.ts index be02d08ec..10b736e11 100644 --- a/libs/api/src/lib/query-and-mutation/index.ts +++ b/libs/api/src/lib/query-and-mutation/index.ts @@ -48,11 +48,11 @@ type UseApiMutationResult = { }; export function useApiMutation(): [ - (args: ApiQueryArgs) => Promise<{ data: TData }>, + (args: ApiQueryArgs) => Promise<{ data: TData }> & { unwrap: () => Promise }, UseApiMutationResult, ] { return useApiMutationMutation() as unknown as [ - (args: ApiQueryArgs) => Promise<{ data: TData }>, + (args: ApiQueryArgs) => Promise<{ data: TData }> & { unwrap: () => Promise }, UseApiMutationResult, ]; }