update usermanagement to legacy mode

This commit is contained in:
mengstabketemaw
2026-06-18 11:25:26 +03:00
parent 3a528c9515
commit 2feaaf1ee4
13 changed files with 158 additions and 57 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { authStorage } from '@ema-platform/auth';
/**
* Same-origin host for the user-management module.
@@ -18,18 +19,13 @@ import { useNavigate, useLocation } from 'react-router-dom';
*/
function readToken(): string | null {
return (
localStorage.getItem('ema-backoffice-auth-token') ??
(() => {
const escaped = 'auth-token'.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
})()
);
return authStorage.getToken() ?? (match ? decodeURIComponent(match[1]) : null);
}
function readRefreshToken(): string | null {
return localStorage.getItem('ema-backoffice-refresh-token') ?? null;
return authStorage.getRefreshToken() ?? null;
}
export default function UserManagementHostPage() {

View File

@@ -1,13 +1 @@
import { Navigate, Outlet } from 'react-router-dom';
function getTokenFromCookie(): string | undefined {
const match = document.cookie.match(/(?:^|;\s*)auth-token=([^;]*)/);
return match ? decodeURIComponent(match[1]) : undefined;
}
export function ProtectedRoute() {
const token =
localStorage.getItem('ema-backoffice-auth-token') ?? getTokenFromCookie();
if (!token) return <Navigate to="/login" replace />;
return <Outlet />;
}
export { ProtectedRoute } from '@ema-platform/auth';

View File

@@ -1,6 +1,13 @@
import { configureStore } from '@reduxjs/toolkit';
import { baseApi } from '@ema-platform/api';
import { authReducer, signupReducer, configureAuthStorage, authStorage } from '@ema-platform/auth';
import { baseApi, configureTokenRefresh } from '@ema-platform/api';
import {
authReducer,
signupReducer,
configureAuthStorage,
authStorage,
refreshAccessToken,
logout,
} from '@ema-platform/auth';
import type { AuthUser } from '@ema-platform/auth';
configureAuthStorage('ema-backoffice');
@@ -25,5 +32,13 @@ export const store = configureStore({
getDefaultMiddleware().concat(baseApi.middleware),
});
configureTokenRefresh({
onTokenExpired: refreshAccessToken,
onAuthFailure: () => {
store.dispatch(logout());
window.location.href = '/login';
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@@ -1,13 +1 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector } from '../store/hooks';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAppSelector((s) => s.auth.isAuthenticated);
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
export { ProtectedRoute } from '@ema-platform/auth';

View File

@@ -1,6 +1,13 @@
import { configureStore } from '@reduxjs/toolkit';
import { baseApi } from '@ema-platform/api';
import { authReducer, signupReducer, configureAuthStorage, authStorage } from '@ema-platform/auth';
import { baseApi, configureTokenRefresh } from '@ema-platform/api';
import {
authReducer,
signupReducer,
configureAuthStorage,
authStorage,
refreshAccessToken,
logout,
} from '@ema-platform/auth';
import type { AuthUser } from '@ema-platform/auth';
configureAuthStorage('ema-portal');
@@ -25,5 +32,13 @@ export const store = configureStore({
getDefaultMiddleware().concat(baseApi.middleware),
});
configureTokenRefresh({
onTokenExpired: refreshAccessToken,
onAuthFailure: () => {
store.dispatch(logout());
window.location.href = '/login';
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@@ -1,3 +1,4 @@
export * from './lib/base-api';
export * from './lib/query-and-mutation';
export * from './lib/session';
export { baseQueryWithReauth, configureTokenRefresh } from './lib/base-api/base-query-with-reauth';

View File

@@ -0,0 +1,53 @@
import { fetchBaseQuery, type BaseQueryFn } from '@reduxjs/toolkit/query/react';
import type { FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query';
import { resolveSessionContext } from '../session';
const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3001/api';
let _onTokenExpired: (() => Promise<string>) | null = null;
let _onAuthFailure: (() => void) | null = null;
export function configureTokenRefresh(config: {
onTokenExpired: () => Promise<string>;
onAuthFailure: () => void;
}) {
_onTokenExpired = config.onTokenExpired;
_onAuthFailure = config.onAuthFailure;
}
export const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
const baseQuery = fetchBaseQuery({
baseUrl: BASE_API_URL,
prepareHeaders: (headers) => {
const { token, sessionHeaders } = resolveSessionContext(
api.getState() as { auth?: { token?: string } },
);
if (token) headers.set('Authorization', `Bearer ${token}`);
Object.entries(sessionHeaders).forEach(([k, v]) => headers.set(k, v));
return headers;
},
});
let result = await baseQuery(args, api, extraOptions);
if (result.error?.status === 401) {
if (_onTokenExpired) {
try {
await _onTokenExpired();
result = await baseQuery(args, api, extraOptions);
} catch {
_onAuthFailure?.();
}
} else {
_onAuthFailure?.();
}
}
return result;
};

View File

@@ -1,23 +1,9 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { resolveSessionContext } from '../session';
const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3001/api';
import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQueryWithReauth } from './base-query-with-reauth';
export const baseApi = createApi({
reducerPath: 'baseApi',
baseQuery: fetchBaseQuery({
baseUrl: BASE_API_URL,
prepareHeaders: (headers, { getState }) => {
const { token, sessionHeaders } = resolveSessionContext(
getState() as { auth?: { token?: string } },
);
if (token) headers.set('Authorization', `Bearer ${token}`);
Object.entries(sessionHeaders).forEach(([k, v]) => headers.set(k, v));
return headers;
},
}),
baseQuery: baseQueryWithReauth,
tagTypes: ['Api'],
endpoints: () => ({}),
});

View File

@@ -1,6 +1,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 { LoginPage } from './lib/pages/LoginPage';
export { SignupPage } from './lib/pages/SignupPage';
export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
@@ -8,4 +9,5 @@ export { OTPVerificationPage } from './lib/pages/OTPVerificationPage';
export { authReducer, loginSuccess, setUser, logout, hydrateAuth } from './lib/store/auth.slice';
export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice';
export { configureAuthStorage, authStorage } from './lib/utils/auth-storage';
export { refreshAccessToken } from './lib/utils/refresh-token';
export type { AuthUser, AuthState, LoginPayload } from './lib/types/auth.types';

View File

@@ -0,0 +1,24 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import type { ReactNode } from 'react';
import { authStorage } from '../utils/auth-storage';
interface ProtectedRouteProps {
children?: ReactNode;
loginPath?: string;
}
function getTokenFromCookie(): string | undefined {
const match = document.cookie.match(/(?:^|;\s*)auth-token=([^;]*)/);
return match ? decodeURIComponent(match[1]) : undefined;
}
export function ProtectedRoute({ children, loginPath = '/login' }: ProtectedRouteProps) {
const location = useLocation();
const token = authStorage.getToken() ?? getTokenFromCookie();
if (!token) {
return <Navigate to={loginPath} state={{ from: location }} replace />;
}
return children ? <>{children}</> : <Outlet />;
}

View File

@@ -25,5 +25,7 @@ export const authStorage = {
[key('auth-token'), key('refresh-token'), key('auth-user')].forEach((k) =>
localStorage.removeItem(k),
);
document.cookie =
'auth-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax';
},
};

View File

@@ -0,0 +1,31 @@
import { authStorage } from './auth-storage';
const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3001/api';
interface RefreshResponse {
token: string;
refreshToken: string;
}
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`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
authStorage.clear();
throw new Error('Token refresh failed');
}
const data: RefreshResponse = await response.json();
authStorage.setToken(data.token);
if (data.refreshToken) authStorage.setRefreshToken(data.refreshToken);
return data.token;
}

View File

@@ -112,7 +112,7 @@ export const projectTheme: DesignConfig = {
// Each value is also exposed as a --um-* CSS var, so tweaks apply instantly.
// ─────────────────────────────────────────────────────────────────────────
layout: {
userManagementView: "classic", // TODO: "legacy" for the top-tab UI
userManagementView: "legacy", // "legacy" top-tab UI, "classic" → side menu
// showTopBar: false, // TODO: overrides VITE_SHOW_TOP_BAR
// ── Dimensions ──────────────────────────────────────────────────────────