admin page fix
This commit is contained in:
parent
d08a02b6e7
commit
b64b7a3880
|
|
@ -13,12 +13,18 @@ export const App = () => {
|
|||
const [, expand] = useExpand();
|
||||
|
||||
useEffect(() => {
|
||||
WebApp.enableClosingConfirmation();
|
||||
expand();
|
||||
if (!WebApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
WebApp.setHeaderColor('#1d1d1b');
|
||||
WebApp.setBackgroundColor('#1d1d1b');
|
||||
}, []);
|
||||
WebApp.enableClosingConfirmation?.();
|
||||
if (typeof expand === 'function') {
|
||||
expand();
|
||||
}
|
||||
|
||||
WebApp.setHeaderColor?.('#1d1d1b');
|
||||
WebApp.setBackgroundColor?.('#1d1d1b');
|
||||
}, [WebApp, expand]);
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,29 @@ const formatDate = (iso?: string | null) => {
|
|||
return dateTimeFormatter.format(date);
|
||||
};
|
||||
|
||||
const formatUnknown = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return "—";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value || "—";
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "bigint") {
|
||||
return numberFormatter.format(Number(value));
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "Да" : "Нет";
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return formatDate(value.toISOString());
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (error) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const Section = ({
|
||||
id,
|
||||
title,
|
||||
|
|
@ -661,7 +684,7 @@ export const AdminPage = () => {
|
|||
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Окружение</h3>
|
||||
<dl className="mt-3 space-y-2">
|
||||
{Object.entries(env).map(([key, value]) => (
|
||||
<InfoRow key={key} label={key}>{value ?? "—"}</InfoRow>
|
||||
<InfoRow key={key} label={key}>{formatUnknown(value)}</InfoRow>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
|
@ -702,8 +725,8 @@ export const AdminPage = () => {
|
|||
{service_config.map((item) => (
|
||||
<tr key={item.key}>
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-300">{item.key}</td>
|
||||
<td className="px-3 py-2">{item.value ?? "—"}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-500">{item.raw ?? "—"}</td>
|
||||
<td className="px-3 py-2">{formatUnknown(item.value)}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-500">{formatUnknown(item.raw)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -770,7 +793,7 @@ export const AdminPage = () => {
|
|||
<tr key={task.id} className="hover:bg-slate-900/50">
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-300">{task.id}</td>
|
||||
<td className="px-3 py-2">{task.destination || "—"}</td>
|
||||
<td className="px-3 py-2">{task.amount || "—"}</td>
|
||||
<td className="px-3 py-2">{formatUnknown(task.amount)}</td>
|
||||
<td className="px-3 py-2"><Badge tone={task.status === "done" ? "success" : task.status === "failed" ? "danger" : "neutral"}>{task.status}</Badge></td>
|
||||
<td className="px-3 py-2">{task.epoch ?? "—"} · {task.seqno ?? "—"}</td>
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-500">{task.transaction_hash || "—"}</td>
|
||||
|
|
@ -836,7 +859,7 @@ export const AdminPage = () => {
|
|||
</td>
|
||||
<td className="px-3 py-2">{node.version || "—"}</td>
|
||||
<td className="px-3 py-2">{formatDate(node.last_seen)}</td>
|
||||
<td className="px-3 py-2">{node.notes || "—"}</td>
|
||||
<td className="px-3 py-2">{formatUnknown(node.notes)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
const TOKEN_STORAGE_KEY = '__admin_panel_token__';
|
||||
const HEADER_STORAGE_KEY = '__admin_panel_header__';
|
||||
const COOKIE_STORAGE_KEY = '__admin_panel_cookie__';
|
||||
const DEFAULT_HEADER_NAME = 'X-Admin-Token';
|
||||
const DEFAULT_COOKIE_NAME = 'admin_session';
|
||||
const DEFAULT_MAX_AGE = 172800; // 48h
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
export type AdminAuthSnapshot = {
|
||||
token: string | null;
|
||||
headerName: string;
|
||||
cookieName: string;
|
||||
maxAge: number;
|
||||
};
|
||||
|
||||
export const getAdminAuthSnapshot = (): AdminAuthSnapshot => {
|
||||
if (!isBrowser) {
|
||||
return {
|
||||
token: null,
|
||||
headerName: DEFAULT_HEADER_NAME,
|
||||
cookieName: DEFAULT_COOKIE_NAME,
|
||||
maxAge: DEFAULT_MAX_AGE,
|
||||
};
|
||||
}
|
||||
|
||||
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
const headerName = window.localStorage.getItem(HEADER_STORAGE_KEY) || DEFAULT_HEADER_NAME;
|
||||
const cookieName = window.localStorage.getItem(COOKIE_STORAGE_KEY) || DEFAULT_COOKIE_NAME;
|
||||
return {
|
||||
token,
|
||||
headerName,
|
||||
cookieName,
|
||||
maxAge: DEFAULT_MAX_AGE,
|
||||
};
|
||||
};
|
||||
|
||||
export const persistAdminAuth = ({
|
||||
token,
|
||||
headerName,
|
||||
cookieName,
|
||||
maxAge,
|
||||
}: {
|
||||
token: string;
|
||||
headerName?: string | null;
|
||||
cookieName?: string | null;
|
||||
maxAge?: number | null;
|
||||
}) => {
|
||||
if (!isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
if (headerName) {
|
||||
window.localStorage.setItem(HEADER_STORAGE_KEY, headerName);
|
||||
}
|
||||
if (cookieName) {
|
||||
window.localStorage.setItem(COOKIE_STORAGE_KEY, cookieName);
|
||||
}
|
||||
|
||||
// Best effort: mirror cookie locally to support same-origin deployments.
|
||||
const targetCookieName = cookieName || DEFAULT_COOKIE_NAME;
|
||||
const cookieParts = [
|
||||
`${targetCookieName}=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
`Max-Age=${maxAge ?? DEFAULT_MAX_AGE}`,
|
||||
'SameSite=Lax',
|
||||
];
|
||||
if (window.location.protocol === 'https:') {
|
||||
cookieParts.push('Secure');
|
||||
}
|
||||
document.cookie = cookieParts.join('; ');
|
||||
};
|
||||
|
||||
export const clearAdminAuth = () => {
|
||||
if (!isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = getAdminAuthSnapshot();
|
||||
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
window.localStorage.removeItem(HEADER_STORAGE_KEY);
|
||||
window.localStorage.removeItem(COOKIE_STORAGE_KEY);
|
||||
|
||||
if (snapshot.cookieName) {
|
||||
const cookieParts = [
|
||||
`${snapshot.cookieName}=`,
|
||||
'Path=/',
|
||||
'Max-Age=0',
|
||||
'SameSite=Lax',
|
||||
];
|
||||
if (window.location.protocol === 'https:') {
|
||||
cookieParts.push('Secure');
|
||||
}
|
||||
document.cookie = cookieParts.join('; ');
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue