diff --git a/apps/portal/src/app/features/auth/pages/LoginPage.tsx b/libs/auth/src/lib/pages/LoginPage.tsx
similarity index 81%
rename from apps/portal/src/app/features/auth/pages/LoginPage.tsx
rename to libs/auth/src/lib/pages/LoginPage.tsx
index eb5d0e907..dcea227b9 100644
--- a/apps/portal/src/app/features/auth/pages/LoginPage.tsx
+++ b/libs/auth/src/lib/pages/LoginPage.tsx
@@ -21,12 +21,13 @@ 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 } from '../../../store/hooks';
-import { loginSuccess, setUser } from '../store/auth.slice';
-import type { LoginPayload, AuthUser } from '../types/auth.types';
+import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
+import { loginSuccess, setUser } from '../store/auth.slice';
+import type { LoginPayload, AuthUser } from '../types/auth.types';
+import { useAuthConfig } from '../AuthConfig';
const schema = z.object({
email: z.string().email({ message: 'Enter a valid email' }),
@@ -37,7 +38,9 @@ type FormValues = z.infer
;
export function LoginPage() {
const navigate = useNavigate();
- const dispatch = useAppDispatch();
+ const dispatch = useDispatch();
+ const { appName, loginRedirectPath, enableSignup, enableForgotPassword } =
+ useAuthConfig();
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [loginTrigger] = useApiMutation();
@@ -68,7 +71,7 @@ export function LoginPage() {
dispatch(setUser(me));
if (me.isPhoneNumberVerified) {
- navigate('/dashboard');
+ navigate(loginRedirectPath);
} else {
navigate('/otp-verify', {
state: { email: me.email, phoneNumber: me.phoneNumber },
@@ -86,10 +89,10 @@ export function LoginPage() {
- Welcome back
+ Welcome to {appName}
- Sign in to access your maritime licensing dashboard.
+ Sign in to access your account.
@@ -119,14 +122,16 @@ export function LoginPage() {
checked={rememberMe}
onChange={(e) => setRememberMe(e.currentTarget.checked)}
/>
-
- Forgot password?
-
+ {enableForgotPassword && (
+
+ Forgot password?
+
+ )}
);
diff --git a/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx b/libs/auth/src/lib/pages/OTPVerificationPage.tsx
similarity index 96%
rename from apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx
rename to libs/auth/src/lib/pages/OTPVerificationPage.tsx
index b1c3a730e..f39b93dc7 100644
--- a/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx
+++ b/libs/auth/src/lib/pages/OTPVerificationPage.tsx
@@ -19,6 +19,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
+import { useAuthConfig } from '../AuthConfig';
const CODE_LENGTH = 6;
const RESEND_SECONDS = 30;
@@ -34,6 +35,7 @@ type FormValues = z.infer;
export function OTPVerificationPage() {
const navigate = useNavigate();
const location = useLocation();
+ const { loginRedirectPath } = useAuthConfig();
const state = location.state as
| { email?: string; phoneNumber?: string }
| null;
@@ -68,7 +70,7 @@ export function OTPVerificationPage() {
}).unwrap();
notify.success('Phone number verified successfully');
- navigate('/dashboard');
+ navigate(loginRedirectPath);
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
notify.error(msg);
@@ -95,7 +97,7 @@ export function OTPVerificationPage() {
return (
@@ -164,7 +166,7 @@ export function OTPVerificationPage() {
variant="light"
fullWidth
size="md"
- onClick={() => navigate('/dashboard')}
+ onClick={() => navigate(loginRedirectPath)}
>
Skip verification for now
diff --git a/apps/portal/src/app/features/auth/pages/SignupPage.tsx b/libs/auth/src/lib/pages/SignupPage.tsx
similarity index 95%
rename from apps/portal/src/app/features/auth/pages/SignupPage.tsx
rename to libs/auth/src/lib/pages/SignupPage.tsx
index a6499dc19..676a82676 100644
--- a/apps/portal/src/app/features/auth/pages/SignupPage.tsx
+++ b/libs/auth/src/lib/pages/SignupPage.tsx
@@ -23,11 +23,12 @@ 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 } from '../../../store/hooks';
-import { loginSuccess } from '../store/auth.slice';
+import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
+import { loginSuccess } from '../store/auth.slice';
+import { useAuthConfig } from '../AuthConfig';
const schema = z
.object({
@@ -62,7 +63,8 @@ interface SignupPayload {
export function SignupPage() {
const navigate = useNavigate();
- const dispatch = useAppDispatch();
+ const dispatch = useDispatch();
+ const { appName, loginRedirectPath } = useAuthConfig();
const [agreed, setAgreed] = useState(false);
const [signupTrigger, { isLoading: loading }] = useApiMutation<{
token: string;
@@ -106,7 +108,7 @@ export function SignupPage() {
);
if (data.isPhoneNumberVerified) {
- navigate('/dashboard');
+ navigate(loginRedirectPath);
} else {
navigate('/otp-verify', {
state: { email: values.email, phoneNumber: values.phoneNumber },
@@ -120,8 +122,8 @@ export function SignupPage() {
return (
diff --git a/apps/portal/src/app/features/auth/store/auth.slice.ts b/libs/auth/src/lib/store/auth.slice.ts
similarity index 96%
rename from apps/portal/src/app/features/auth/store/auth.slice.ts
rename to libs/auth/src/lib/store/auth.slice.ts
index 48d397b8f..f93ce4ddc 100644
--- a/apps/portal/src/app/features/auth/store/auth.slice.ts
+++ b/libs/auth/src/lib/store/auth.slice.ts
@@ -30,7 +30,7 @@ const authSlice = createSlice({
},
hydrateAuth(state) {
const token = authStorage.getToken();
- const user = authStorage.getUser();
+ const user = authStorage.getUser
();
if (token && user) {
state.token = token;
state.user = user;
diff --git a/apps/portal/src/app/features/auth/store/signup.slice.ts b/libs/auth/src/lib/store/signup.slice.ts
similarity index 100%
rename from apps/portal/src/app/features/auth/store/signup.slice.ts
rename to libs/auth/src/lib/store/signup.slice.ts
diff --git a/apps/portal/src/app/features/auth/types/auth.types.ts b/libs/auth/src/lib/types/auth.types.ts
similarity index 100%
rename from apps/portal/src/app/features/auth/types/auth.types.ts
rename to libs/auth/src/lib/types/auth.types.ts
diff --git a/libs/auth/src/lib/utils/auth-storage.ts b/libs/auth/src/lib/utils/auth-storage.ts
new file mode 100644
index 000000000..19ff15fbf
--- /dev/null
+++ b/libs/auth/src/lib/utils/auth-storage.ts
@@ -0,0 +1,29 @@
+let _prefix = 'ema-auth';
+
+export function configureAuthStorage(prefix: string) {
+ _prefix = prefix;
+}
+
+function key(k: string) {
+ return `${_prefix}-${k}`;
+}
+
+export const authStorage = {
+ getToken: () => localStorage.getItem(key('auth-token')) ?? undefined,
+ setToken: (token: string) => localStorage.setItem(key('auth-token'), token),
+ getRefreshToken: () => localStorage.getItem(key('refresh-token')) ?? undefined,
+ setRefreshToken: (t: string) => localStorage.setItem(key('refresh-token'), t),
+ getUser: (): T | null => {
+ try {
+ return JSON.parse(localStorage.getItem(key('auth-user')) ?? 'null') as T | null;
+ } catch {
+ return null;
+ }
+ },
+ setUser: (u: T) => localStorage.setItem(key('auth-user'), JSON.stringify(u)),
+ clear: () => {
+ [key('auth-token'), key('refresh-token'), key('auth-user')].forEach((k) =>
+ localStorage.removeItem(k),
+ );
+ },
+};
diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json
new file mode 100644
index 000000000..8c54e1fa5
--- /dev/null
+++ b/libs/auth/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "outDir": "../../dist/out-tsc" },
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
+}
diff --git a/libs/shared/src/lib/theme/ema-theme.ts b/libs/shared/src/lib/theme/ema-theme.ts
index 3b4d6cd56..de3b3ca72 100644
--- a/libs/shared/src/lib/theme/ema-theme.ts
+++ b/libs/shared/src/lib/theme/ema-theme.ts
@@ -31,4 +31,7 @@ export const emaTheme = createTheme({
md: '0 4px 20px rgba(15,23,42,0.08)',
lg: '0 8px 30px rgba(15,23,42,0.12)',
},
+ other: {
+ heroGradient: 'linear-gradient(135deg, #3160b7 0%, #1fc29d 100%)',
+ },
});
diff --git a/tsconfig.base.json b/tsconfig.base.json
index fb046a0ce..96df34488 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -19,7 +19,8 @@
"paths": {
"@ema-platform/shared": ["libs/shared/src/index.ts"],
"@ema-platform/ui": ["libs/ui/src/index.ts"],
- "@ema-platform/api": ["libs/api/src/index.ts"]
+ "@ema-platform/api": ["libs/api/src/index.ts"],
+ "@ema-platform/auth": ["libs/auth/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]
diff --git a/user-management-config/fhc.theme.ts b/user-management-config/fhc.theme.ts
index 6620b0b4a..22429eef7 100644
--- a/user-management-config/fhc.theme.ts
+++ b/user-management-config/fhc.theme.ts
@@ -39,7 +39,7 @@ export const FHC_COLORS = {
"#96BDEB",
"#6FA4E0",
"#4A90E2",
- "#357ABD",
+ "#4b7fe5",
"#2C669D",
"#224F7A",
"#173654",
@@ -120,7 +120,7 @@ export const FHC_LAYOUT = {
brick: "#1A3A5C",
brickDark: "#0F2440",
blue: "#4A90E2",
- blueDark: "#357ABD",
+ blueDark: "#4b7fe5",
gold: "#4A90E2",
text: "#1F2937",
},
@@ -216,7 +216,7 @@ export const fhcMantineTheme: MantineThemeOverride = {
* color while still getting the fhc* palettes + layout tokens via `mantineTheme`.
*/
export const fhcDesignPreset = {
- colors: { primary: "#357ABD" },
+ colors: { primary: "#4b7fe5" },
typography: { fontFamily: "Plus Jakarta Sans, sans-serif" },
shape: { radius: "10px" },
mantineTheme: fhcMantineTheme,
diff --git a/user-management-config/project.theme.ts b/user-management-config/project.theme.ts
index a010a44a0..32fd4a78e 100644
--- a/user-management-config/project.theme.ts
+++ b/user-management-config/project.theme.ts
@@ -40,7 +40,7 @@ export const projectTheme: DesignConfig = {
// still have different colors.
// ─────────────────────────────────────────────────────────────────────────
colors: {
- primary: "#357ABD", // FHC blue (fhcBlue-6) — buttons, links, active states
+ primary: "#4b7fe5", // FHC blue (fhcBlue-6) — buttons, links, active states
// // "#5D2E1F" brick (FHC chrome) is used by the sidebar/modal skin below
// // "#2563eb" blue | "#7c3aed" purple
// // "#16a34a" green | "#dc2626" red
@@ -174,18 +174,18 @@ export const projectTheme: DesignConfig = {
// ── Top tab bar skin (legacy view) ─────────────────────────────────────
menuBackground: "#ffffff", // TODO: tab bar background
menuColor: "#334155", // inactive tab text
- menuActiveColor: "#357ABD", // active tab text (FHC blue)
- menuActiveBorderColor: "#357ABD", // active tab underline
- menuHoverColor: "#357ABD", // tab hover text
+ menuActiveColor: "#4b7fe5", // active tab text (FHC blue)
+ menuActiveBorderColor: "#4b7fe5", // active tab underline
+ menuHoverColor: "#4b7fe5", // tab hover text
// ── Create / edit modal skin (shared BackofficeModal) ──────────────────
- modalAccentColor: "#357ABD", // blue top strip
+ modalAccentColor: "#4b7fe5", // blue top strip
modalHeaderBackground: "#EEF4FC", // header bg (view)
modalHeaderEditBackground: "#D9E8FA", // header bg (edit)
modalIconBackground: "#D9E8FA", // header icon chip bg
- modalIconColor: "#357ABD", // header icon chip color
+ modalIconColor: "#4b7fe5", // header icon chip color
modalTitleColor: "#1F2937", // modal title text
- modalFocusColor: "#357ABD", // input focus ring inside modals
+ modalFocusColor: "#4b7fe5", // input focus ring inside modals
modalSurface: "#ffffff", // modal body surface
},