feat(i18n): update medical verification label to include sea service verification

fix(layouts): import baseApi in BackofficeLayout and PortalLayout for API state management

refactor(auth): remove clearCurrentProfile call on logout and improve cookie removal logic

chore(auth-storage): enhance cookie management with secure and sameSite attributes, and streamline cleanup process
This commit is contained in:
Estifo77
2026-08-10 14:42:21 +03:00
parent 1a0da134a5
commit 4fc73b0567
6 changed files with 1001 additions and 906 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@ export const en = {
applications: 'Applications',
paymentConfig: 'Payment Config',
analytics: 'Analytics',
medicalVerification: 'Medical Verification',
medicalVerification: 'Medical and Sea Service Verification',
locations: 'Locations',
configuration: 'Configuration',
profile: 'Profile',

View File

@@ -8,7 +8,7 @@ import { AppHeader, AppSidebar } from '@ema-platform/ui';
import type { NavItem, NavSection } from '@ema-platform/ui';
import { notify } from '@ema-platform/ui';
import { AppTopNav, filterByPermissions } from '@ema-platform/ui';
import { useGetQueueCountsQuery } from '@ema-platform/api';
import { baseApi, useGetQueueCountsQuery } from '@ema-platform/api';
import { usePermissions } from '@ema-platform/auth';
import { SUPPORTED_LANGUAGES } from '../i18n/config';
import { useAppDispatch, useAppSelector } from '../store/hooks';

View File

@@ -22,7 +22,7 @@ import { useDispatch } from "react-redux";
import { notify, AppHeader, AppSidebar } from "@ema-platform/ui";
import type { NavItem } from "@ema-platform/ui";
import { BrandMark, logout } from "@ema-platform/auth";
import { useGetUnseenNotificationsQuery } from "@ema-platform/api";
import { baseApi, useGetUnseenNotificationsQuery } from "@ema-platform/api";
import { SUPPORTED_LANGUAGES } from "../i18n/config";
import { useAppSelector } from "../store/hooks";
@@ -172,6 +172,7 @@ export function PortalLayout() {
const handleLogout = () => {
dispatch(logout());
dispatch(baseApi.util.resetApiState());
navigate("/login");
};

View File

@@ -42,7 +42,6 @@ const authSlice = createSlice({
state.isAuthenticated = false;
state.currentProfile = null;
authStorage.clear();
clearCurrentProfile();
},
setToken(state, action: PayloadAction<string>) {
state.token = action.payload;

View File

@@ -1,6 +1,75 @@
import Cookies from "js-cookie";
const rememberMe = localStorage.getItem("rememberMe") === "true";
const COOKIE_EXPIRES_DAYS = rememberMe ? 15 : 1;
function getCookieExpiresDays(): number {
if (typeof window === "undefined") return 1;
try {
const rememberMe = localStorage.getItem("rememberMe") === "true";
return rememberMe ? 15 : 1;
} catch {
return 1;
}
}
const isHttps =
typeof window !== "undefined" && window.location.protocol === "https:";
function removeCookieCompletely(name: string) {
if (typeof document === "undefined") return;
const paths = ["/", ""];
const samesites: Array<"strict" | "lax" | "none" | undefined> = [
"strict",
"lax",
undefined,
];
const secures = [true, false];
for (const path of paths) {
for (const secure of secures) {
for (const sameSite of samesites) {
try {
Cookies.remove(name, { path, secure, sameSite });
} catch {
// ignore
}
}
}
}
const pastDate = "Thu, 01 Jan 1970 00:00:00 GMT";
const expireStrings = [
`${name}=; path=/; expires=${pastDate}; max-age=0`,
`${name}=; path=/; expires=${pastDate}; max-age=0; SameSite=Strict`,
`${name}=; path=/; expires=${pastDate}; max-age=0; SameSite=Strict; Secure`,
`${name}=; path=/; expires=${pastDate}; max-age=0; SameSite=Lax`,
`${name}=; path=/; expires=${pastDate}; max-age=0; SameSite=Lax; Secure`,
`${name}=; expires=${pastDate}; max-age=0`,
];
try {
const domain = window.location.hostname;
const domainParts = domain.split(".");
if (domainParts.length > 1) {
expireStrings.push(
`${name}=; path=/; domain=${domain}; expires=${pastDate}; max-age=0`
);
expireStrings.push(
`${name}=; path=/; domain=.${domain}; expires=${pastDate}; max-age=0`
);
}
} catch {
// ignore
}
expireStrings.forEach((str) => {
try {
document.cookie = str;
} catch {
// ignore
}
});
}
interface Backend {
get(k: string): string | null;
set(k: string, v: string): void;
@@ -15,15 +84,14 @@ const localStore: Backend = {
const cookieStore: Backend = {
get: (k) => Cookies.get(k) ?? null,
set: (k, v) =>
Cookies.set(k, v, {
path: "/",
secure: true,
secure: isHttps,
sameSite: "strict",
expires: COOKIE_EXPIRES_DAYS,
expires: getCookieExpiresDays(),
}),
remove: (k) => Cookies.remove(k, { path: "/" }),
remove: (k) => removeCookieCompletely(k),
};
const AUTH_KEY_NAMES = [
@@ -34,6 +102,8 @@ const AUTH_KEY_NAMES = [
"current-profile",
];
const KNOWN_PREFIXES = ["ema-backoffice", "ema-portal", "ema-auth", ""];
let _prefix = "ema-auth";
let backend: Backend = localStore;
@@ -41,7 +111,7 @@ export function configureAuthStorage(prefix: string, useCookies = false) {
_prefix = prefix;
backend = useCookies ? cookieStore : localStore;
if (useCookies) {
// one-time cleanup: drop pre-migration localStorage leftovers so tokens live in cookies only
// drop pre-migration localStorage leftovers so tokens live in cookies only
AUTH_KEY_NAMES.forEach((k) => localStorage.removeItem(`${prefix}-${k}`));
}
}
@@ -78,17 +148,36 @@ export const authStorage = {
backend.set(key("current-profile"), JSON.stringify(p)),
removeProfile: () => backend.remove(key("current-profile")),
clear: () => {
[
key("auth-token"),
key("refresh-token"),
key("auth-user"),
key("profile-id"),
key("current-profile"),
key("profile-id"),
].forEach((k) => backend.remove(k));
Object.keys(Cookies.get()).forEach((name) => {
Cookies.remove(name);
Cookies.remove(name, { path: "/" });
const prefixes = Array.from(new Set([_prefix, ...KNOWN_PREFIXES]));
prefixes.forEach((p) => {
AUTH_KEY_NAMES.forEach((k) => {
const fullKey = p ? `${p}-${k}` : k;
removeCookieCompletely(fullKey);
try {
localStorage.removeItem(fullKey);
} catch {
// ignore
}
});
});
try {
localStorage.removeItem("rememberMe");
} catch {
// ignore
}
try {
if (typeof Cookies !== "undefined") {
const allCookies = Cookies.get();
if (allCookies) {
Object.keys(allCookies).forEach((name) => {
removeCookieCompletely(name);
});
}
}
} catch {
// ignore
}
},
};