mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
remove backoffice custom authentication
This commit is contained in:
BIN
apps/backoffice/public/assets/emaLogo.jpg
Normal file
BIN
apps/backoffice/public/assets/emaLogo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -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<LoginPayload, LoginArgs>({
|
||||
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;
|
||||
@@ -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<typeof schema>;
|
||||
|
||||
export function LoginForm() {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
const [loginMutate, { isLoading }] = useLoginMutation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
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 (
|
||||
<Paper p="xl" shadow="md" radius="md">
|
||||
<Stack gap="lg">
|
||||
<Stack gap={4}>
|
||||
<Title order={2}>Sign in</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Enter your credentials to access the backoffice
|
||||
</Text>
|
||||
</Stack>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="you@example.com"
|
||||
error={errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
error={errors.password?.message}
|
||||
{...register('password')}
|
||||
/>
|
||||
<Button type="submit" loading={isLoading} fullWidth mt="sm">
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<Text ta="center" size="sm" c="dimmed">
|
||||
Don't have an account?{' '}
|
||||
<Anchor component={Link} to="/signup">
|
||||
Sign up
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Login as LoginPage } from '@tria-plc/iamui-common';
|
||||
@@ -1 +0,0 @@
|
||||
export { SetPasswordPage } from '@tria-plc/iamui-common';
|
||||
@@ -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<string, string> }).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<typeof schema>;
|
||||
|
||||
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<FormValues>({
|
||||
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 (
|
||||
<Paper p="xl" shadow="md" radius="md">
|
||||
<Stack gap="lg">
|
||||
<Stack gap={4}>
|
||||
<Title order={2}>Create an account</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Fill in your details to get started
|
||||
</Text>
|
||||
</Stack>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Full Name"
|
||||
placeholder="Your full name"
|
||||
error={errors.name?.message}
|
||||
{...register('name')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="you@example.com"
|
||||
error={errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
error={errors.username?.message}
|
||||
{...register('username')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Phone Number"
|
||||
placeholder="+251 911 234 567"
|
||||
error={errors.phoneNumber?.message}
|
||||
{...register('phoneNumber')}
|
||||
/>
|
||||
<TextInput
|
||||
label="User Type"
|
||||
placeholder="e.g. admin, manager"
|
||||
error={errors.userType?.message}
|
||||
{...register('userType')}
|
||||
/>
|
||||
<Button type="submit" loading={loading} fullWidth mt="sm">
|
||||
Sign up
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<Text ta="center" size="sm" c="dimmed">
|
||||
Already have an account?{' '}
|
||||
<Anchor component={Link} to="/login">
|
||||
Sign in
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -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<LoginPayload>) {
|
||||
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;
|
||||
@@ -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<string>) {
|
||||
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;
|
||||
@@ -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<string>) {
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -48,11 +48,11 @@ type UseApiMutationResult<TData> = {
|
||||
};
|
||||
|
||||
export function useApiMutation<TData = unknown>(): [
|
||||
(args: ApiQueryArgs) => Promise<{ data: TData }>,
|
||||
(args: ApiQueryArgs) => Promise<{ data: TData }> & { unwrap: () => Promise<TData> },
|
||||
UseApiMutationResult<TData>,
|
||||
] {
|
||||
return useApiMutationMutation() as unknown as [
|
||||
(args: ApiQueryArgs) => Promise<{ data: TData }>,
|
||||
(args: ApiQueryArgs) => Promise<{ data: TData }> & { unwrap: () => Promise<TData> },
|
||||
UseApiMutationResult<TData>,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user