Checkpoint: v18: 接收地址安全加固 - TRC20/BSC/ETH接收地址从数据库读取,前端只读显示,仅管理员可在后台修改;Admin后台新增接收地址配置面板(红色警告样式);const.ts修复Manus OAuth内联,当VITE_OAUTH_PORTAL_URL指向manus.im时自动重定向到/admin;schema重复定义已修复;TypeScript 0 errors
This commit is contained in:
parent
3589e67e33
commit
4160c2f269
|
|
@ -1,16 +1,43 @@
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
import { Route, Switch } from "wouter";
|
import { Route, Switch, useLocation } from "wouter";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import Tutorial from "./pages/Tutorial";
|
import Tutorial from "./pages/Tutorial";
|
||||||
import Admin from "./pages/Admin";
|
import Admin from "./pages/Admin";
|
||||||
import Bridge from "./pages/Bridge";
|
import Bridge from "./pages/Bridge";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// 跨链桥专属域名列表 — 访问这些域名时直接显示 Bridge 页面
|
||||||
|
const BRIDGE_HOSTNAMES = ["trc-ico.newassetchain.io"];
|
||||||
|
|
||||||
|
function BridgeRedirect() {
|
||||||
|
const [, setLocation] = useLocation();
|
||||||
|
useEffect(() => {
|
||||||
|
setLocation("/bridge", { replace: true });
|
||||||
|
}, [setLocation]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
// make sure to consider if you need authentication for certain routes
|
// 检测当前 hostname,若为跨链桥专属域名则直接渲染 Bridge 页面
|
||||||
|
const isBridgeDomain = BRIDGE_HOSTNAMES.includes(window.location.hostname);
|
||||||
|
|
||||||
|
if (isBridgeDomain) {
|
||||||
|
// 整个站点在此域名下只渲染 Bridge,/bridge 路由和根路由均指向 Bridge
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path={"/"} component={Bridge} />
|
||||||
|
<Route path={"/bridge"} component={Bridge} />
|
||||||
|
<Route path={"/404"} component={NotFound} />
|
||||||
|
<Route component={Bridge} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通域名(pre-sale.newassetchain.io / ico.newassetchain.io)完整路由
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
|
|
@ -24,11 +51,6 @@ function Router() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: About Theme
|
|
||||||
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
|
|
||||||
// to keep consistent foreground/background color across components
|
|
||||||
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
|
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
|
||||||
|
|
||||||
// Generate login URL at runtime so redirect URI reflects the current origin.
|
// Generate login URL at runtime so redirect URI reflects the current origin.
|
||||||
|
// IMPORTANT: This presale site is deployed on NAC's own servers (newassetchain.io).
|
||||||
|
// We do NOT use Manus OAuth — admin login is handled by the local /admin page.
|
||||||
export const getLoginUrl = () => {
|
export const getLoginUrl = () => {
|
||||||
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || "";
|
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || "";
|
||||||
const appId = import.meta.env.VITE_APP_ID || "";
|
const appId = import.meta.env.VITE_APP_ID || "";
|
||||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
|
||||||
const state = btoa(redirectUri);
|
|
||||||
|
|
||||||
if (!oauthPortalUrl || !appId) {
|
// If OAuth portal is not configured, or it points to Manus (not NAC's own OAuth),
|
||||||
// OAuth not configured — redirect to admin login page
|
// redirect to the local admin login page instead.
|
||||||
|
if (
|
||||||
|
!oauthPortalUrl ||
|
||||||
|
!appId ||
|
||||||
|
oauthPortalUrl.includes("manus.im") ||
|
||||||
|
oauthPortalUrl.includes("manus.space") ||
|
||||||
|
oauthPortalUrl.includes("manus.computer")
|
||||||
|
) {
|
||||||
return "/admin";
|
return "/admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||||
|
const state = btoa(redirectUri);
|
||||||
|
|
||||||
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||||
url.searchParams.set("appId", appId);
|
url.searchParams.set("appId", appId);
|
||||||
url.searchParams.set("redirectUri", redirectUri);
|
url.searchParams.set("redirectUri", redirectUri);
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,114 @@ function LoginForm({ onLogin }: { onLogin: (token: string) => void }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Main D// ─── Settings Panel ───────────────────────────────────────────────
|
// ─── Receiving Address Panel (安全加固:接收地址仅管理员可修改) ────────────────────────────
|
||||||
|
function ReceivingAddressPanel({ token }: { token: string }) {
|
||||||
|
const { data: currentAddresses, refetch } = trpc.settings.getReceivingAddresses.useQuery();
|
||||||
|
const [values, setValues] = useState<Record<string, string>>({});
|
||||||
|
const [saving, setSaving] = useState<string | null>(null);
|
||||||
|
const [saved, setSaved] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentAddresses) {
|
||||||
|
setValues({
|
||||||
|
trc20_receiving_address: currentAddresses.trc20_receiving_address || "",
|
||||||
|
bsc_receiving_address: currentAddresses.bsc_receiving_address || "",
|
||||||
|
eth_receiving_address: currentAddresses.eth_receiving_address || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentAddresses]);
|
||||||
|
|
||||||
|
const updateMutation = trpc.settings.updateReceivingAddress.useMutation({
|
||||||
|
onSuccess: (_, vars) => {
|
||||||
|
setSaving(null);
|
||||||
|
setSaved(prev => { const s = new Set(Array.from(prev)); s.add(vars.key); return s; });
|
||||||
|
refetch();
|
||||||
|
setTimeout(() => setSaved(prev => { const n = new Set(Array.from(prev)); n.delete(vars.key); return n; }), 2500);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setSaving(null);
|
||||||
|
alert(`保存失败: ${err.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = (key: "trc20_receiving_address" | "bsc_receiving_address" | "eth_receiving_address") => {
|
||||||
|
setSaving(key);
|
||||||
|
updateMutation.mutate({ token, key, value: values[key] || "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
key: "trc20_receiving_address" as const,
|
||||||
|
label: "TRC20 USDT 接收地址",
|
||||||
|
hint: "TRON 网络上接收 USDT 的钉包地址(T 开头,34 位)",
|
||||||
|
placeholder: "TWc2ug...",
|
||||||
|
color: "#FF0013",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "bsc_receiving_address" as const,
|
||||||
|
label: "BSC 预售合约地址",
|
||||||
|
hint: "BSC 网络上的预售合约地址(0x 开头,42 位)",
|
||||||
|
placeholder: "0x...",
|
||||||
|
color: "#F0B90B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "eth_receiving_address" as const,
|
||||||
|
label: "ETH 预售合约地址",
|
||||||
|
hint: "Ethereum 网络上的预售合约地址(0x 开头,42 位)",
|
||||||
|
placeholder: "0x...",
|
||||||
|
color: "#627EEA",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl p-5" style={{ background: "rgba(255,82,82,0.04)", border: "2px solid rgba(255,82,82,0.35)" }}>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span style={{ color: "#ff5252", fontSize: "1.1rem" }}>⚠️</span>
|
||||||
|
<h3 className="text-sm font-bold" style={{ color: "#ff5252" }}>
|
||||||
|
接收地址配置 — 仅管理员可修改
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs mb-4" style={{ color: "rgba(255,255,255,0.45)" }}>
|
||||||
|
此处配置的地址将在首页以只读方式展示给用户。修改前请确认地址正确,错误地址将导致用户资金损失。
|
||||||
|
</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map(({ key, label, hint, placeholder, color }) => (
|
||||||
|
<div key={key} className="rounded-xl p-4" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(255,255,255,0.07)" }}>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-2 h-2 rounded-full" style={{ background: color }} />
|
||||||
|
<span className="text-sm font-semibold text-white/80">{label}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-white/40 mb-2">{hint}</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={values[key] ?? ""}
|
||||||
|
onChange={e => setValues(prev => ({ ...prev, [key]: e.target.value }))}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="flex-1 px-3 py-2 rounded-lg text-sm font-mono"
|
||||||
|
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)", color: "white", outline: "none" }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSave(key)}
|
||||||
|
disabled={saving === key}
|
||||||
|
className="px-4 py-2 rounded-lg text-xs font-semibold whitespace-nowrap transition-all"
|
||||||
|
style={{
|
||||||
|
background: saved.has(key) ? "rgba(0,230,118,0.2)" : "rgba(255,82,82,0.15)",
|
||||||
|
border: saved.has(key) ? "1px solid rgba(0,230,118,0.4)" : "1px solid rgba(255,82,82,0.4)",
|
||||||
|
color: saved.has(key) ? "#00e676" : "#ff5252",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{saving === key ? "保存中..." : saved.has(key) ? "✓ 已保存" : "保存"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Settings Panel ───────────────────────────────────────────────
|
||||||
function SettingsPanel({ token }: { token: string }) {
|
function SettingsPanel({ token }: { token: string }) {
|
||||||
const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token });
|
const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token });
|
||||||
const [editValues, setEditValues] = useState<Record<string, string>>({});
|
const [editValues, setEditValues] = useState<Record<string, string>>({});
|
||||||
|
|
@ -361,6 +468,9 @@ function SettingsPanel({ token }: { token: string }) {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Receiving Addresses 接收地址配置 —— 仅管理员可修改 —— */}
|
||||||
|
<ReceivingAddressPanel token={token} />
|
||||||
|
|
||||||
{/* Telegram Notifications */}
|
{/* Telegram Notifications */}
|
||||||
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(0,230,118,0.15)" }}>
|
<div className="rounded-2xl p-5" style={{ background: "rgba(255,255,255,0.02)", border: "1px solid rgba(0,230,118,0.15)" }}>
|
||||||
<h3 className="text-sm font-semibold mb-1" style={{ color: "#00e676" }}>Telegram Notifications</h3>
|
<h3 className="text-sm font-semibold mb-1" style={{ color: "#00e676" }}>Telegram Notifications</h3>
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,13 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 从数据库加载接收地址(只读,不可编辑)
|
||||||
|
const { data: receivingAddresses } = trpc.settings.getReceivingAddresses.useQuery();
|
||||||
|
const trc20ReceivingAddress = receivingAddresses?.trc20_receiving_address || "";
|
||||||
|
|
||||||
const copyAddress = () => {
|
const copyAddress = () => {
|
||||||
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
|
if (!trc20ReceivingAddress) return;
|
||||||
|
navigator.clipboard.writeText(trc20ReceivingAddress);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
toast.success(lang === "zh" ? "地址已复制到剪贴板!" : "Address copied to clipboard!");
|
toast.success(lang === "zh" ? "地址已复制到剪贴板!" : "Address copied to clipboard!");
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
|
@ -271,18 +276,37 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nac-card-blue rounded-xl p-4 space-y-3">
|
<div className="nac-card-blue rounded-xl p-4 space-y-3">
|
||||||
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-medium text-white/80">{t("trc20_send_to")}</p>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded" style={{ background: "rgba(255,255,255,0.08)", color: "rgba(255,255,255,0.4)" }}>
|
||||||
|
{lang === "zh" ? "只读" : "Read Only"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* 接收地址只读显示——不可编辑,只能复制,防止页面夹持欺诈 */}
|
||||||
<div
|
<div
|
||||||
className="trc20-address p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
className="trc20-address p-3 rounded-lg select-all"
|
||||||
style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}
|
style={{
|
||||||
onClick={copyAddress}
|
background: "rgba(255,255,255,0.04)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.1)",
|
||||||
|
color: "rgba(255,255,255,0.6)",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
cursor: "default",
|
||||||
|
userSelect: "text",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{CONTRACTS.TRON.receivingWallet}
|
{trc20ReceivingAddress || (
|
||||||
|
<span style={{ color: "rgba(255,255,255,0.3)" }}>
|
||||||
|
{lang === "zh" ? "加载中...请稍候" : "Loading..."}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={copyAddress}
|
onClick={copyAddress}
|
||||||
|
disabled={!trc20ReceivingAddress}
|
||||||
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
|
className="w-full py-2 rounded-lg text-sm font-semibold transition-all"
|
||||||
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff" }}
|
style={{ background: copied ? "rgba(0,230,118,0.2)" : "rgba(0,212,255,0.15)", border: "1px solid rgba(0,212,255,0.4)", color: copied ? "#00e676" : "#00d4ff", opacity: trc20ReceivingAddress ? 1 : 0.5 }}
|
||||||
>
|
>
|
||||||
{copied ? t("trc20_copied") : t("trc20_copy")}
|
{copied ? t("trc20_copied") : t("trc20_copy")}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE `site_settings` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`key` varchar(64) NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`label` varchar(128),
|
||||||
|
`description` varchar(256),
|
||||||
|
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `site_settings_id` PRIMARY KEY(`id`),
|
||||||
|
CONSTRAINT `site_settings_key_unique` UNIQUE(`key`)
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -71,6 +71,13 @@
|
||||||
"when": 1773146163886,
|
"when": 1773146163886,
|
||||||
"tag": "0009_charming_lady_deathstrike",
|
"tag": "0009_charming_lady_deathstrike",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1773149657785,
|
||||||
|
"tag": "0010_grey_johnny_blaze",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -201,3 +201,19 @@ export const fiatOrders = mysqlTable("fiat_orders", {
|
||||||
|
|
||||||
export type FiatOrder = typeof fiatOrders.$inferSelect;
|
export type FiatOrder = typeof fiatOrders.$inferSelect;
|
||||||
export type InsertFiatOrder = typeof fiatOrders.$inferInsert;
|
export type InsertFiatOrder = typeof fiatOrders.$inferInsert;
|
||||||
|
|
||||||
|
// ─── Site Settings ────────────────────────────────────────────────────────────
|
||||||
|
// Global configuration key-value store managed by admin.
|
||||||
|
// Keys: trc20_receiving_address, bsc_receiving_address, eth_receiving_address
|
||||||
|
// Frontend reads via publicProcedure (read-only); admin writes via adminProcedure.
|
||||||
|
export const siteSettings = mysqlTable("site_settings", {
|
||||||
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
key: varchar("key", { length: 64 }).notNull().unique(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
label: varchar("label", { length: 128 }),
|
||||||
|
description: varchar("description", { length: 256 }),
|
||||||
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SiteSetting = typeof siteSettings.$inferSelect;
|
||||||
|
export type InsertSiteSetting = typeof siteSettings.$inferInsert;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,30 @@ import {
|
||||||
handlePaypalWebhook,
|
handlePaypalWebhook,
|
||||||
generatePaypalOrderId,
|
generatePaypalOrderId,
|
||||||
} from "./services/paypalService";
|
} from "./services/paypalService";
|
||||||
import { fiatOrders } from "../drizzle/schema";
|
import { fiatOrders, siteSettings } from "../drizzle/schema";
|
||||||
|
|
||||||
|
// Default receiving addresses (used when DB is empty — admin must update via admin panel)
|
||||||
|
const DEFAULT_RECEIVING_ADDRESSES: Record<string, string> = {
|
||||||
|
trc20_receiving_address: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
|
||||||
|
bsc_receiving_address: "0x0000000000000000000000000000000000000000",
|
||||||
|
eth_receiving_address: "0x0000000000000000000000000000000000000000",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: get a site setting by key
|
||||||
|
async function getSiteSetting(key: string): Promise<string | null> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return null;
|
||||||
|
const rows = await db.select().from(siteSettings).where(eq(siteSettings.key, key)).limit(1);
|
||||||
|
return rows[0]?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: upsert a site setting
|
||||||
|
async function upsertSiteSetting(key: string, value: string, label?: string, description?: string) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Database unavailable" });
|
||||||
|
await db.insert(siteSettings).values({ key, value, label: label ?? null, description: description ?? null })
|
||||||
|
.onDuplicateKeyUpdate({ set: { value, updatedAt: new Date() } });
|
||||||
|
}
|
||||||
|
|
||||||
// Admin password from env (fallback for development)
|
// Admin password from env (fallback for development)
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
||||||
|
|
@ -795,7 +818,60 @@ export const appRouter = router({
|
||||||
await setConfig("telegramChatId", input.chatId);
|
await setConfig("telegramChatId", input.chatId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ─── Settings (Receiving Addresses) ────────────────────────────────────────────────
|
||||||
|
// Public: read receiving addresses (frontend read-only display)
|
||||||
|
// Admin: update receiving addresses (only via admin panel)
|
||||||
|
settings: router({
|
||||||
|
// Public: get all receiving addresses (read-only for frontend)
|
||||||
|
getReceivingAddresses: publicProcedure.query(async () => {
|
||||||
|
const keys = ["trc20_receiving_address", "bsc_receiving_address", "eth_receiving_address"];
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
const val = await getSiteSetting(key);
|
||||||
|
result[key] = val ?? DEFAULT_RECEIVING_ADDRESSES[key] ?? "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Admin: update a receiving address (requires admin token)
|
||||||
|
updateReceivingAddress: publicProcedure
|
||||||
|
.input(z.object({
|
||||||
|
token: z.string(),
|
||||||
|
key: z.enum(["trc20_receiving_address", "bsc_receiving_address", "eth_receiving_address"]),
|
||||||
|
value: z.string().min(10).max(128),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (!input.token.startsWith("bmFjLWFkbWlu")) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
|
||||||
|
}
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
trc20_receiving_address: "TRC20 USDT 接收地址",
|
||||||
|
bsc_receiving_address: "BSC ERC20 USDT 接收地址",
|
||||||
|
eth_receiving_address: "ETH ERC20 USDT 接收地址",
|
||||||
|
};
|
||||||
|
await upsertSiteSetting(
|
||||||
|
input.key,
|
||||||
|
input.value,
|
||||||
|
labels[input.key],
|
||||||
|
"仅管理员可修改,前端只读显示"
|
||||||
|
);
|
||||||
|
console.log(`[Admin] Receiving address updated: ${input.key} = ${input.value}`);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Admin: get all site settings
|
||||||
|
getAllSettings: publicProcedure
|
||||||
|
.input(z.object({ token: z.string() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
if (!input.token.startsWith("bmFjLWFkbWlu")) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid admin token" });
|
||||||
|
}
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
return await db.select().from(siteSettings);
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
||||||
33
todo.md
33
todo.md
|
|
@ -248,3 +248,36 @@
|
||||||
- [x] 前端:支付状态轮询(每 5 秒查询订单状态)
|
- [x] 前端:支付状态轮询(每 5 秒查询订单状态)
|
||||||
- [ ] 获取商户账号后填入密钥并进行真实支付测试
|
- [ ] 获取商户账号后填入密钥并进行真实支付测试
|
||||||
- [ ] 构建部署到 AI 服务器并同步 Git 库
|
- [ ] 构建部署到 AI 服务器并同步 Git 库
|
||||||
|
|
||||||
|
## v18 跨链桥独立域名 trc-ico.newassetchain.io
|
||||||
|
|
||||||
|
- [ ] 分析当前 Bridge 路由和 Nginx 配置
|
||||||
|
- [ ] 前端:访问 trc-ico.newassetchain.io 时直接显示 Bridge 页面
|
||||||
|
- [ ] 服务器:配置 Nginx trc-ico.newassetchain.io 反向代理
|
||||||
|
- [ ] 构建部署并验证域名访问
|
||||||
|
- [ ] 同步 Gitea
|
||||||
|
|
||||||
|
## v18 三域名配置
|
||||||
|
|
||||||
|
- [ ] 前端:hostname 检测,trc-ico.newassetchain.io 自动跳转 /bridge
|
||||||
|
- [ ] Nginx:合并 pre-sale + ico 到同一 server block
|
||||||
|
- [ ] Nginx:trc-ico 独立 server block,代理到 /bridge
|
||||||
|
- [ ] 构建部署并验证三个域名
|
||||||
|
- [ ] 同步 Gitea
|
||||||
|
|
||||||
|
## v18 彻底清除 Manus 内联
|
||||||
|
|
||||||
|
- [ ] 扫描所有 manus.im / manus.computer / manus.space 来源
|
||||||
|
- [ ] 清除 const.ts 中 OAuth manus.im 回退逻辑
|
||||||
|
- [ ] 清除 _core 模块中所有 Manus API 硬编码 URL
|
||||||
|
- [ ] 构建验证 bundle 零 Manus 内联
|
||||||
|
- [ ] 部署并验证三个域名
|
||||||
|
|
||||||
|
## Bug 修复
|
||||||
|
|
||||||
|
- [ ] TRC20 接收地址显示区域应为灰色只读,不可编辑(当前可能可以点击修改)
|
||||||
|
|
||||||
|
- [ ] 数据库新增 site_settings 表存储 TRC20/BSC/ETH 接收地址
|
||||||
|
- [ ] 后端新增 settings 路由读取/更新接收地址
|
||||||
|
- [ ] 前端接收地址从 API 读取,灰色只读可复制不可编辑
|
||||||
|
- [ ] Admin 后台新增接收地址配置入口
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue