mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Adding license generation functionality
This commit is contained in:
@@ -2,6 +2,7 @@ export { AuthConfigProvider, useAuthConfig } from './lib/AuthConfig';
|
||||
export type { AuthConfigValue } from './lib/AuthConfig';
|
||||
export { AuthShell, BrandMark } from './lib/components/AuthShell';
|
||||
export { ProtectedRoute } from './lib/components/ProtectedRoute';
|
||||
export { AuthBootstrap } from './lib/components/AuthBootstrap';
|
||||
export { LoginPage } from './lib/pages/LoginPage';
|
||||
export { SignupPage } from './lib/pages/SignupPage';
|
||||
export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
|
||||
|
||||
70
libs/auth/src/lib/components/AuthBootstrap.tsx
Normal file
70
libs/auth/src/lib/components/AuthBootstrap.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { authStorage } from '../utils/auth-storage';
|
||||
import { hydrateAuth, logout, setUser } from '../store/auth.slice';
|
||||
import type { AuthUser } from '../types/auth.types';
|
||||
|
||||
const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3001/api';
|
||||
|
||||
/**
|
||||
* Restores the signed-in session before the router renders.
|
||||
*
|
||||
* Redux starts empty on every page load while the token lives in
|
||||
* localStorage, so without this a refresh leaves the app half–signed-in: the
|
||||
* route guards see a token and let you through, but pages that read
|
||||
* `auth.user` think you are a stranger.
|
||||
*
|
||||
* A token the server no longer accepts is cleared here rather than left to
|
||||
* strand the user on a page they cannot act on or sign out of.
|
||||
*/
|
||||
export function AuthBootstrap({ children }: { children: ReactNode }) {
|
||||
const dispatch = useDispatch();
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function restore() {
|
||||
dispatch(hydrateAuth());
|
||||
|
||||
const token = authStorage.getToken();
|
||||
if (!token) {
|
||||
if (!cancelled) setReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_API_URL}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const user = (await response.json()) as AuthUser;
|
||||
if (!cancelled) dispatch(setUser(user));
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
// Expired or revoked — drop it so the user gets a login screen
|
||||
// instead of a dead end.
|
||||
if (!cancelled) dispatch(logout());
|
||||
}
|
||||
} catch {
|
||||
// Offline or the API is down: keep the stored session and let the
|
||||
// individual screens surface their own errors.
|
||||
} finally {
|
||||
if (!cancelled) setReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
restore();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Rendering the router before the session resolves would let the guards
|
||||
// redirect based on a state that is about to change.
|
||||
if (!ready) return null;
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -74,7 +74,8 @@ export function LoginPage() {
|
||||
}).unwrap();
|
||||
dispatch(setUser(me));
|
||||
|
||||
let hasProfile = false;
|
||||
// Load the seafarer profile if one exists, so those screens have it —
|
||||
// but never gate sign-in on it.
|
||||
try {
|
||||
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
|
||||
const result = await profileCheckTrigger({
|
||||
@@ -85,10 +86,10 @@ export function LoginPage() {
|
||||
const profile = result.items[0];
|
||||
authStorage.setProfileId(profile.id);
|
||||
dispatch(setCurrentProfile(profile));
|
||||
hasProfile = true;
|
||||
}
|
||||
} catch {
|
||||
// profile not found — redirect to setup
|
||||
// No profile yet. That is fine — a profile is only needed by the
|
||||
// seafarer features, not to apply for a licence.
|
||||
}
|
||||
|
||||
if (!me.isPhoneNumberVerified) {
|
||||
@@ -96,17 +97,11 @@ export function LoginPage() {
|
||||
state: {
|
||||
email: me.email,
|
||||
phoneNumber: me.phoneNumber,
|
||||
needsProfile: !hasProfile,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasProfile) {
|
||||
navigate('/profile-setup');
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(loginRedirectPath);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
|
||||
@@ -38,11 +38,10 @@ export function OTPVerificationPage() {
|
||||
const location = useLocation();
|
||||
const { loginRedirectPath } = useAuthConfig();
|
||||
const state = location.state as
|
||||
| { email?: string; phoneNumber?: string; needsProfile?: boolean }
|
||||
| { email?: string; phoneNumber?: string }
|
||||
| null;
|
||||
const email = state?.email ?? '';
|
||||
const phoneNumber = state?.phoneNumber ?? '';
|
||||
const needsProfile = state?.needsProfile ?? false;
|
||||
|
||||
const [verifyTrigger, { isLoading: loading }] = useApiMutation();
|
||||
const [resendTrigger, { isLoading: resending }] = useApiMutation();
|
||||
@@ -73,7 +72,7 @@ export function OTPVerificationPage() {
|
||||
}).unwrap();
|
||||
|
||||
notify.success('Phone number verified successfully');
|
||||
navigate(needsProfile ? '/profile-setup' : loginRedirectPath);
|
||||
navigate(loginRedirectPath);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
@@ -142,7 +141,7 @@ export function OTPVerificationPage() {
|
||||
<Stack gap={6} align="center">
|
||||
<PinInput
|
||||
length={CODE_LENGTH}
|
||||
type="text"
|
||||
type="alphanumeric"
|
||||
inputMode="text"
|
||||
oneTimeCode
|
||||
size="md"
|
||||
|
||||
@@ -115,13 +115,12 @@ export function SignupPage() {
|
||||
dispatch(setUser(me));
|
||||
|
||||
if (data.isPhoneNumberVerified) {
|
||||
navigate('/profile-setup');
|
||||
navigate(loginRedirectPath);
|
||||
} else {
|
||||
navigate('/otp-verify', {
|
||||
state: {
|
||||
email: values.email,
|
||||
phoneNumber: values.phoneNumber,
|
||||
needsProfile: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface CurrentProfileAddress {
|
||||
postalAddress: string | null;
|
||||
emergencyContactName: string | null;
|
||||
emergencyContactPhone: string | null;
|
||||
emergencycontactRelation: string | null;
|
||||
emergencyContactRelation: string | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ export async function refreshAccessToken(): Promise<string> {
|
||||
const refreshToken = authStorage.getRefreshToken();
|
||||
if (!refreshToken) throw new Error('No refresh token available');
|
||||
|
||||
const response = await fetch(`${BASE_API_URL}/auth/refresh`, {
|
||||
// The IAM service exposes this as `refresh-token`; posting to `/auth/refresh`
|
||||
// 404s, which the catch below turns into a silent logout.
|
||||
const response = await fetch(`${BASE_API_URL}/auth/refresh-token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
|
||||
Reference in New Issue
Block a user