admin page fix
This commit is contained in:
parent
d08a02b6e7
commit
b64b7a3880
|
|
@ -13,12 +13,18 @@ export const App = () => {
|
||||||
const [, expand] = useExpand();
|
const [, expand] = useExpand();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
WebApp.enableClosingConfirmation();
|
if (!WebApp) {
|
||||||
expand();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
WebApp.setHeaderColor('#1d1d1b');
|
WebApp.enableClosingConfirmation?.();
|
||||||
WebApp.setBackgroundColor('#1d1d1b');
|
if (typeof expand === 'function') {
|
||||||
}, []);
|
expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
WebApp.setHeaderColor?.('#1d1d1b');
|
||||||
|
WebApp.setBackgroundColor?.('#1d1d1b');
|
||||||
|
}, [WebApp, expand]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Providers>
|
<Providers>
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,29 @@ const formatDate = (iso?: string | null) => {
|
||||||
return dateTimeFormatter.format(date);
|
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 = ({
|
const Section = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
|
@ -661,7 +684,7 @@ export const AdminPage = () => {
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Окружение</h3>
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-400">Окружение</h3>
|
||||||
<dl className="mt-3 space-y-2">
|
<dl className="mt-3 space-y-2">
|
||||||
{Object.entries(env).map(([key, value]) => (
|
{Object.entries(env).map(([key, value]) => (
|
||||||
<InfoRow key={key} label={key}>{value ?? "—"}</InfoRow>
|
<InfoRow key={key} label={key}>{formatUnknown(value)}</InfoRow>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -702,8 +725,8 @@ export const AdminPage = () => {
|
||||||
{service_config.map((item) => (
|
{service_config.map((item) => (
|
||||||
<tr key={item.key}>
|
<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 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">{formatUnknown(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 font-mono text-xs text-slate-500">{formatUnknown(item.raw)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -770,7 +793,7 @@ export const AdminPage = () => {
|
||||||
<tr key={task.id} className="hover:bg-slate-900/50">
|
<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 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.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"><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">{task.epoch ?? "—"} · {task.seqno ?? "—"}</td>
|
||||||
<td className="px-3 py-2 font-mono text-xs text-slate-500">{task.transaction_hash || "—"}</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>
|
||||||
<td className="px-3 py-2">{node.version || "—"}</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">{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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</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