Use a shared dashboard login token store

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mrinal 2026-11-10 05:31:41 +00:00
parent 5b6ebe8918
commit 7fd0dcb8eb
14 changed files with 230 additions and 145 deletions

View file

@ -1740,7 +1740,7 @@
"count": 5
},
"no-restricted-syntax": {
"count": 132
"count": 131
},
"prefer-const": {
"count": 26
@ -2079,11 +2079,6 @@
"count": 1
}
},
"src/contexts/AuthContext.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/contexts/ThemeContext.tsx": {
"no-restricted-syntax": {
"count": 1

View file

@ -1,6 +1,6 @@
"use client";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
import { clearTokenCookies, getCookie, useAuthToken } from "@/utils/cookieUtils";
import { checkTokenValidity, decodeToken } from "@/utils/jwtUtils";
import { useEffect, useMemo } from "react";
import { effectiveSessionRole, formatUserRole, isViewOnlySessionRole, userRoleDisplayLabel } from "@/utils/roles";
@ -11,7 +11,7 @@ import { useUIConfig } from "./uiConfig/useUIConfig";
const useAuthorized = () => {
const { data: uiConfig, isLoading: isUIConfigLoading } = useUIConfig();
const token = typeof document !== "undefined" ? getCookie("token") : null;
const token = useAuthToken();
const decoded = useMemo(() => decodeToken(token), [token]);
const isTokenValid = useMemo(() => checkTokenValidity(token), [token]);
@ -29,7 +29,7 @@ const useAuthorized = () => {
if (isLoading) return;
if (!isAuthorized) {
if (token) {
if (token || getCookie("token")) {
clearTokenCookies();
}
redirectToLogin();

View file

@ -21,6 +21,13 @@ const currentUserRequests = () =>
let searchParamsValue = new URLSearchParams();
const sessionCookie = (claims: Record<string, unknown>) => {
const encode = (part: Record<string, unknown>) =>
btoa(JSON.stringify(part)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const exp = Math.floor(Date.now() / 1000) + 3600;
return `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ ...claims, exp })}.sig`;
};
vi.mock("@/lib/telemetry/uiEvents", () => ({
recordUiEvent: vi.fn(() => Promise.resolve()),
}));
@ -98,9 +105,12 @@ describe("(dashboard) Layout", () => {
pathOf(input) === CURRENT_USER_PATH ? currentUserReply() : Promise.resolve(Response.json({}, { status: 404 })),
);
vi.stubGlobal("fetch", fetchMock);
clearTokenCookies();
storeLoginToken(sessionCookie({ user_id: "normal-user", key: "sk-normal-session" }));
});
afterEach(() => {
clearTokenCookies();
vi.unstubAllGlobals();
});
@ -234,13 +244,6 @@ describe("(dashboard) Layout", () => {
});
describe("forced password reset routing", () => {
const sessionCookie = (claims: Record<string, unknown>) => {
const encode = (part: Record<string, unknown>) =>
btoa(JSON.stringify(part)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const exp = Math.floor(Date.now() / 1000) + 3600;
return `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ ...claims, exp })}.sig`;
};
afterEach(() => {
clearTokenCookies();
document.cookie = "token=; Max-Age=0; Path=/";
@ -253,6 +256,18 @@ describe("(dashboard) Layout", () => {
return <div>{userEmail ? `${userEmail} (${userRole})` : "Signed out"}</div>;
}
function SessionDetails() {
const { authLoading, token, userID, userEmail, userRole, accessToken, premiumUser, showSSOBanner } = useAuth();
if (authLoading) return <div>Loading session</div>;
return (
<div>
{token ? "Token present" : "No token"} | {userID ?? "No user"} | {userEmail ?? "No email"} |{" "}
{userRole || "No role"} | {accessToken ?? "No key"} | {premiumUser ? "Premium" : "Standard"} |{" "}
{showSSOBanner ? "SSO banner" : "No SSO banner"}
</div>
);
}
it("updates the mounted session after login and when a different user signs in", async () => {
clearTokenCookies();
render(
@ -270,6 +285,69 @@ describe("(dashboard) Layout", () => {
expect(await screen.findByText("user@example.test (Internal User)")).toBeInTheDocument();
});
it("clears the previous principal's missing email and premium status when switching users", async () => {
clearTokenCookies();
const adminClaims = {
user_id: "admin",
user_email: "admin@example.test",
user_role: "proxy_admin",
key: "admin-key",
premium_user: true,
login_method: "sso",
};
storeLoginToken(sessionCookie(adminClaims));
render(
<AuthProvider>
<SessionDetails />
</AuthProvider>,
);
pendingUiConfig.resolve();
expect(
await screen.findByText(
"Token present | admin | admin@example.test | Admin | admin-key | Premium | No SSO banner",
),
).toBeInTheDocument();
act(() =>
storeLoginToken(sessionCookie({ user_id: "internal", user_role: "internal_user", key: "internal-key" })),
);
expect(
await screen.findByText(
"Token present | internal | No email | Internal User | internal-key | Standard | SSO banner",
),
).toBeInTheDocument();
});
it("clears the token and all identity fields for mounted consumers on sign out", async () => {
clearTokenCookies();
const adminClaims = {
user_id: "admin",
user_email: "admin@example.test",
user_role: "proxy_admin",
key: "admin-key",
premium_user: true,
};
storeLoginToken(sessionCookie(adminClaims));
render(
<AuthProvider>
<SessionDetails />
</AuthProvider>,
);
pendingUiConfig.resolve();
expect(
await screen.findByText(
"Token present | admin | admin@example.test | Admin | admin-key | Premium | SSO banner",
),
).toBeInTheDocument();
act(() => clearTokenCookies());
expect(
await screen.findByText("No token | No user | No email | No role | No key | Standard | SSO banner"),
).toBeInTheDocument();
});
it.each(["", " ", "invalid-token"])("does not authenticate a stored invalid token %j", async (token) => {
clearTokenCookies();
render(
@ -390,6 +468,33 @@ describe("(dashboard) Layout", () => {
}
});
it("redirects a signed-out session without mounting the shell", async () => {
clearTokenCookies();
const originalLocation = window.location;
const locationReplace = vi.fn();
Object.defineProperty(window, "location", {
value: { ...originalLocation, replace: locationReplace },
writable: true,
});
try {
render(
<AuthProvider>
<Layout>
<div data-testid="page-content" />
</Layout>
</AuthProvider>,
);
pendingUiConfig.resolve();
await waitFor(() => expect(locationReplace).toHaveBeenCalledWith(expect.stringContaining("/login")));
expect(screen.queryByTestId("sidebar")).not.toBeInTheDocument();
expect(screen.queryByTestId("page-content")).not.toBeInTheDocument();
expect(currentUserRequests()).toHaveLength(0);
} finally {
Object.defineProperty(window, "location", { value: originalLocation, writable: true });
}
});
it("keeps the session of an invitation link whose current user cannot be loaded", async () => {
const originalLocation = window.location;
const locationReplace = vi.fn();

View file

@ -240,13 +240,14 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { accessToken, token, authLoading, passwordResetRequired } = useAuth();
const redirectingToLogin = useRef(false);
const session = decodeToken(token);
const { isLoading: currentUserLoading, isError: currentUserFailed } = useCurrentUser(
session?.key ?? null,
session?.auth_header_name,
);
const isInvitationFlow = Boolean(searchParams.get("invitation_id"));
const identityPending = currentUserLoading || currentUserFailed;
const identityPending = !token || currentUserLoading || currentUserFailed;
// Legacy invitation links point at /ui/?invitation_id=; the onboarding form now lives at its own
// /onboarding route. Redirect once ui-config has loaded so uiHref resolves the SERVER_ROOT_PATH base.
@ -265,10 +266,12 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
}, [authLoading, passwordResetRequired, pathname, router]);
useEffect(() => {
if (!currentUserFailed || isInvitationFlow) return;
clearTokenCookies();
if (authLoading || isInvitationFlow || redirectingToLogin.current) return;
if (token && !currentUserFailed) return;
redirectingToLogin.current = true;
if (currentUserFailed) clearTokenCookies();
redirectToLogin();
}, [currentUserFailed, isInvitationFlow]);
}, [authLoading, currentUserFailed, isInvitationFlow, token]);
if (authLoading || identityPending || isInvitationFlow) {
return <LoadingScreen />;

View file

@ -20,6 +20,7 @@ vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({
vi.mock("@/utils/cookieUtils", () => ({
clearTokenCookies: vi.fn(),
getCookieFromDocument: vi.fn(),
getAuthToken: vi.fn(() => null),
}));
vi.mock("@/utils/jwtUtils", () => ({

View file

@ -8,7 +8,7 @@ import { renderWithProviders, screen, testQueryClient, waitFor, within } from ".
import ModelHubTable from "./ModelHubTable";
const mockUseUISettings = vi.hoisted(() => vi.fn());
const mockGetCookie = vi.hoisted(() => vi.fn());
const mockUseAuthToken = vi.hoisted(() => vi.fn());
const mockCheckTokenValidity = vi.hoisted(() => vi.fn());
const mockRouterReplace = vi.hoisted(() => vi.fn());
const mockLocationReplace = vi.hoisted(() => vi.fn());
@ -73,7 +73,7 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
}));
vi.mock("@/utils/cookieUtils", () => ({
getCookie: mockGetCookie,
useAuthToken: mockUseAuthToken,
}));
vi.mock("@/utils/jwtUtils", () => ({
@ -117,7 +117,7 @@ describe("ModelHubTable", () => {
},
isLoading: false,
});
mockGetCookie.mockReturnValue(tokenValue);
mockUseAuthToken.mockReturnValue(tokenValue);
mockCheckTokenValidity.mockReturnValue(isTokenValid);
mockRouterReplace.mockClear();
mockLocationReplace.mockClear();

View file

@ -46,7 +46,7 @@ import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { checkTokenValidity } from "@/utils/jwtUtils";
import { getCookie } from "@/utils/cookieUtils";
import { useAuthToken } from "@/utils/cookieUtils";
import { getLoginUrl } from "@/utils/returnUrlUtils";
import { uiHref } from "@/utils/uiHref";
import {
@ -154,6 +154,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
const [skillLoading, setSkillLoading] = useState<boolean>(false);
const [isMakeSkillPublicModalVisible, setIsMakeSkillPublicModalVisible] = useState(false);
const { data: uiSettings, isLoading: isUISettingsLoading } = useUISettings();
const token = useAuthToken();
// Check authentication requirement for public AI Hub
useEffect(() => {
@ -166,7 +167,6 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
// If require_auth_for_public_ai_hub is true, verify token
if (requireAuth === true) {
const token = getCookie("token");
const isTokenValid = checkTokenValidity(token);
// If token is invalid, redirect to login
@ -176,7 +176,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({
}
}
// If require_auth_for_public_ai_hub is false, allow public access (no change)
}, [isUISettingsLoading, publicPage, uiSettings]);
}, [isUISettingsLoading, publicPage, uiSettings, token]);
useEffect(() => {
const fetchData = async (accessToken: string) => {

View file

@ -6,7 +6,7 @@ import { getProxyBaseUrl } from "@/components/networking";
import i18n from "@/i18n/i18n";
import { extractProxyErrorMessage } from "@/lib/http/client";
import { toast } from "@/lib/toast";
import { getCookie } from "@/utils/cookieUtils";
import { getAuthToken } from "@/utils/cookieUtils";
import { checkTokenValidity } from "@/utils/jwtUtils";
import { runLiteAdmin } from "./agent";
import type { ActionResult, AriaAccess, LiteAdminAction } from "./operations";
@ -119,7 +119,7 @@ export function useLiteAdmin(session: LiteAdminSession) {
setEntries((current) => [...current, { kind: "message", id: message.id, message }]);
setPhase("thinking");
const sameSession = () =>
getCookie("token") === session.token &&
getAuthToken() === session.token &&
checkTokenValidity(session.token) &&
getProxyBaseUrl() === session.managementBaseUrl;
const assertCurrent = () => {

View file

@ -5,7 +5,7 @@ import { uiHref } from "@/utils/uiHref";
vi.mock("@/utils/cookieUtils", () => ({
clearTokenCookies: vi.fn(),
getCookie: vi.fn(),
getAuthToken: vi.fn(),
storeLoginToken: vi.fn(),
}));

View file

@ -88,7 +88,7 @@ export const getAutoRouterAssembledPromptCall = async (
*/
import i18n from "@/i18n/i18n";
import { toast } from "@/lib/toast";
import { clearTokenCookies, getCookie, storeLoginToken } from "@/utils/cookieUtils";
import { clearTokenCookies, getAuthToken, storeLoginToken } from "@/utils/cookieUtils";
import { decodeToken } from "@/utils/jwtUtils";
import { normalizeBackendPageUrlToUiOrigin } from "@/utils/returnUrlUtils";
import { TagNewRequest, TagUpdateRequest, TagListResponse, TagInfoResponse } from "./tag_management/types";
@ -545,7 +545,7 @@ const gatewayApiClient = createApiClient({
registerBaseUrlGetter(getProxyBaseUrl);
registerAuthHeaderNameGetter(getGlobalLitellmHeaderName);
registerAuthTokenGetter(() => decodeToken(getCookie("token"))?.key ?? null);
registerAuthTokenGetter(() => decodeToken(getAuthToken())?.key ?? null);
registerErrorHandler(handleError);
export const makeModelGroupPublic = async (accessToken: string, modelGroups: string[]) => {

View file

@ -4,16 +4,17 @@ import { useEffect } from "react";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { getProxyBaseUrl, getUiPageUrl } from "@/components/networking";
import { getCookie } from "@/utils/cookieUtils";
import { useAuthToken } from "@/utils/cookieUtils";
import { checkTokenValidity } from "@/utils/jwtUtils";
import { getLoginUrl } from "@/utils/returnUrlUtils";
export function useRequirePublicHubAuth() {
const { data: uiSettings, isLoading } = useUISettings();
const token = useAuthToken();
const requireAuth = !isLoading && uiSettings?.values?.require_auth_for_public_ai_hub === true;
useEffect(() => {
if (requireAuth && !checkTokenValidity(getCookie("token"))) {
if (requireAuth && !checkTokenValidity(token)) {
window.location.replace(getUiPageUrl(getLoginUrl(getProxyBaseUrl())));
}
}, [requireAuth]);
}, [requireAuth, token]);
}

View file

@ -1,18 +1,22 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from "react";
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import { jwtDecode } from "jwt-decode";
import { clearTokenCookies, getCookie, LOGIN_TOKEN_CHANGED_EVENT } from "@/utils/cookieUtils";
import { isJwtExpired } from "@/utils/jwtUtils";
import { clearTokenCookies, getAuthToken, getCookie, useAuthToken } from "@/utils/cookieUtils";
import { effectiveSessionRole } from "@/utils/roles";
import { getUiConfig, setGlobalLitellmHeaderName } from "@/components/networking";
function deleteCookie(name: string, path = "/") {
document.cookie = `${name}=; Max-Age=0; Path=${path}`;
if (name === "token") {
clearTokenCookies();
}
}
type SessionClaims = {
key?: string;
user_id?: string;
user_role?: string;
user_email?: string;
premium_user?: boolean;
disabled_non_admin_personal_key_creation?: boolean;
login_method?: string;
password_reset_required?: boolean;
auth_header_name?: string;
};
type AuthContextValue = {
authLoading: boolean;
@ -25,132 +29,52 @@ type AuthContextValue = {
disabledPersonalKeyCreation: boolean;
showSSOBanner: boolean;
passwordResetRequired: boolean;
setToken: React.Dispatch<React.SetStateAction<string | null>>;
setUserID: React.Dispatch<React.SetStateAction<string | null>>;
setUserRole: React.Dispatch<React.SetStateAction<string>>;
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
setAccessToken: React.Dispatch<React.SetStateAction<string | null>>;
setPremiumUser: React.Dispatch<React.SetStateAction<boolean>>;
setShowSSOBanner: React.Dispatch<React.SetStateAction<boolean>>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [authLoading, setAuthLoading] = useState(true);
const [token, setToken] = useState<string | null>(null);
const [userID, setUserID] = useState<string | null>(null);
const [userRole, setUserRole] = useState("");
const [userEmail, setUserEmail] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [premiumUser, setPremiumUser] = useState(false);
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false);
const [showSSOBanner, setShowSSOBanner] = useState(true);
const [passwordResetRequired, setPasswordResetRequired] = useState(false);
const token = useAuthToken();
const decoded = useMemo(() => (token ? jwtDecode<SessionClaims>(token) : null), [token]);
// Load runtime UI config (populates proxyBaseUrl etc.) before clearing
// authLoading, so any consumer that builds proxy-rooted URLs from authLoading=false
// (e.g. the unauthenticated login redirect) sees the resolved value rather than
// the module-init default. Then read the cookie and validate JWT expiry.
useEffect(() => {
let cancelled = false;
const refreshToken = () => {
const raw = getCookie("token");
const valid = raw && !isJwtExpired(raw) ? raw : null;
if (raw && !valid) {
deleteCookie("token", "/");
}
setToken(valid);
};
window.addEventListener(LOGIN_TOKEN_CHANGED_EVENT, refreshToken);
(async () => {
try {
await getUiConfig();
} catch {
// proceed regardless; auth state must still be resolved
} finally {
if (!cancelled) setAuthLoading(false);
}
if (cancelled) return;
refreshToken();
setAuthLoading(false);
})();
})().catch(() => {});
return () => {
cancelled = true;
window.removeEventListener(LOGIN_TOKEN_CHANGED_EVENT, refreshToken);
};
}, []);
// Decode JWT and populate derived auth state whenever the token changes.
useEffect(() => {
if (!token) {
return;
if (getCookie("token") && !getAuthToken()) {
clearTokenCookies();
}
if (isJwtExpired(token)) {
deleteCookie("token", "/");
setToken(null);
return;
}
let decoded: { [k: string]: any } | null = null;
try {
decoded = jwtDecode(token);
} catch {
deleteCookie("token", "/");
setToken(null);
return;
}
if (!decoded) return;
setAccessToken(decoded.key);
setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation);
if (decoded.user_role) {
setUserRole(effectiveSessionRole(decoded.user_role));
}
if (decoded.user_email) {
setUserEmail(decoded.user_email);
}
if (decoded.login_method) {
setShowSSOBanner(decoded.login_method === "username_password");
}
if (decoded.premium_user) {
setPremiumUser(decoded.premium_user);
}
if (decoded.auth_header_name) {
setGlobalLitellmHeaderName(decoded.auth_header_name);
}
if (decoded.user_id) {
setUserID(decoded.user_id);
}
setPasswordResetRequired(decoded.password_reset_required === true);
}, [token]);
useEffect(() => {
setGlobalLitellmHeaderName(decoded?.auth_header_name ?? "Authorization");
}, [decoded?.auth_header_name]);
const value: AuthContextValue = {
authLoading,
token,
userID,
userRole,
userEmail,
accessToken,
premiumUser,
disabledPersonalKeyCreation,
showSSOBanner,
passwordResetRequired,
setToken,
setUserID,
setUserRole,
setUserEmail,
setAccessToken,
setPremiumUser,
setShowSSOBanner,
userID: decoded?.user_id ?? null,
userRole: decoded?.user_role ? effectiveSessionRole(decoded.user_role) : "",
userEmail: decoded?.user_email ?? null,
accessToken: decoded?.key ?? null,
premiumUser: decoded?.premium_user ?? false,
disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? false,
showSSOBanner: decoded?.login_method ? decoded.login_method === "username_password" : true,
passwordResetRequired: decoded?.password_reset_required === true,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

View file

@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi } from "vitest";
import { clearTokenCookies, getCookie, storeLoginToken } from "./cookieUtils";
import { clearTokenCookies, getAuthToken, getCookie, storeLoginToken, subscribeAuthToken } from "./cookieUtils";
import { getToken, setToken } from "./mcpTokenStore";
describe("cookieUtils", () => {
@ -14,6 +14,39 @@ describe("cookieUtils", () => {
vi.spyOn(console, "log").mockImplementation(() => {});
});
describe("auth token snapshots", () => {
const jwt = (exp: number) => `eyJhbGciOiJub25lIn0.${btoa(JSON.stringify({ exp }))}.sig`;
it("reads a stable valid snapshot from the session fallback", () => {
const token = jwt(Math.floor(Date.now() / 1000) + 3600);
sessionStorage.setItem("token", token);
expect(getAuthToken()).toBe(token);
expect(getAuthToken()).toBe(getAuthToken());
});
it.each(["invalid-token", jwt(0)])("returns null without mutating invalid storage for %s", (token) => {
sessionStorage.setItem("token", token);
expect(getAuthToken()).toBeNull();
expect(sessionStorage.getItem("token")).toBe(token);
});
it("notifies subscribers after writes and clears and stops after unsubscribe", () => {
const snapshots: (string | null)[] = [];
const token = jwt(Math.floor(Date.now() / 1000) + 3600);
const unsubscribe = subscribeAuthToken(() => snapshots.push(getAuthToken()));
storeLoginToken(token);
clearTokenCookies();
unsubscribe();
storeLoginToken(token);
expect(snapshots).toEqual([token, null]);
clearTokenCookies();
});
});
describe("clearTokenCookies", () => {
it("should clear token cookie from root path", () => {
document.cookie = "token=test-token-value; path=/";

View file

@ -2,9 +2,31 @@
* Utility functions for managing cookies
*/
import { useSyncExternalStore } from "react";
import { isJwtExpired } from "./jwtUtils";
import { clearAllMcpTokens } from "./mcpTokenStore";
export const LOGIN_TOKEN_CHANGED_EVENT = "litellm-login-token-changed";
const authTokenListeners = new Set<() => void>();
function notifyAuthToken() {
authTokenListeners.forEach((listener) => listener());
}
export function getAuthToken(): string | null {
const token = getCookie("token");
return token && !isJwtExpired(token) ? token : null;
}
export function subscribeAuthToken(listener: () => void): () => void {
authTokenListeners.add(listener);
return () => {
authTokenListeners.delete(listener);
};
}
export function useAuthToken(): string | null {
return useSyncExternalStore(subscribeAuthToken, getAuthToken, () => null);
}
/**
* Returns the cookie path for the UI.
@ -72,6 +94,7 @@ export function clearTokenCookies() {
}
clearAllMcpTokens();
notifyAuthToken();
}
/**
@ -106,7 +129,7 @@ export function storeLoginToken(token: string) {
// sessionStorage may be unavailable (e.g. private browsing quota exceeded)
}
window.dispatchEvent(new Event(LOGIN_TOKEN_CHANGED_EVENT));
notifyAuthToken();
}
/**