Compare commits
2 Commits
3589e67e33
...
570c950718
| Author | SHA1 | Date |
|---|---|---|
|
|
570c950718 | |
|
|
4160c2f269 |
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"query": "\nINSERT INTO site_settings (`key`, value, description) VALUES \n ('bsc_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'BSC BEP-20 USDT 接收地址'),\n ('eth_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'ETH ERC-20 USDT 接收地址'),\n ('polygon_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Polygon USDT 接收地址'),\n ('arbitrum_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Arbitrum USDT 接收地址'),\n ('avalanche_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Avalanche USDT 接收地址'),\n ('trc20_receiving_address', 'TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp', 'TRC20 USDT 接收地址')\nON DUPLICATE KEY UPDATE value = VALUES(value);\n\nSELECT `key`, value FROM site_settings ORDER BY `key`;\n",
|
||||
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway03.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 3Bq4cgN2KNKQqNu.8160cd2033e0 --database Ngki3MumDNGduV3xJt3mga --execute \nINSERT INTO site_settings (`key`, value, description) VALUES \n ('bsc_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'BSC BEP-20 USDT 接收地址'),\n ('eth_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'ETH ERC-20 USDT 接收地址'),\n ('polygon_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Polygon USDT 接收地址'),\n ('arbitrum_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Arbitrum USDT 接收地址'),\n ('avalanche_receiving_address', '0x43DAb577f3279e11D311E7d628C6201d893A9Aa3', 'Avalanche USDT 接收地址'),\n ('trc20_receiving_address', 'TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp', 'TRC20 USDT 接收地址')\nON DUPLICATE KEY UPDATE value = VALUES(value);\n\nSELECT `key`, value FROM site_settings ORDER BY `key`;\n",
|
||||
"rows": [
|
||||
{
|
||||
"key": "arbitrum_receiving_address",
|
||||
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
|
||||
},
|
||||
{
|
||||
"key": "avalanche_receiving_address",
|
||||
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
|
||||
},
|
||||
{
|
||||
"key": "bsc_receiving_address",
|
||||
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
|
||||
},
|
||||
{
|
||||
"key": "eth_receiving_address",
|
||||
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
|
||||
},
|
||||
{
|
||||
"key": "polygon_receiving_address",
|
||||
"value": "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3"
|
||||
},
|
||||
{
|
||||
"key": "trc20_receiving_address",
|
||||
"value": "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp"
|
||||
}
|
||||
],
|
||||
"messages": [],
|
||||
"stdout": "key\tvalue\narbitrum_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\navalanche_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\nbsc_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\neth_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\npolygon_receiving_address\t0x43DAb577f3279e11D311E7d628C6201d893A9Aa3\ntrc20_receiving_address\tTWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp\n",
|
||||
"stderr": "",
|
||||
"execution_time_ms": 1782
|
||||
}
|
||||
|
|
@ -1,16 +1,43 @@
|
|||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import { Route, Switch } from "wouter";
|
||||
import { Route, Switch, useLocation } from "wouter";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import Home from "./pages/Home";
|
||||
import Tutorial from "./pages/Tutorial";
|
||||
import Admin from "./pages/Admin";
|
||||
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() {
|
||||
// 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 (
|
||||
<Switch>
|
||||
<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() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,31 @@
|
|||
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
|
||||
|
||||
// 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 = () => {
|
||||
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || "";
|
||||
const appId = import.meta.env.VITE_APP_ID || "";
|
||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||
const state = btoa(redirectUri);
|
||||
|
||||
if (!oauthPortalUrl || !appId) {
|
||||
// OAuth not configured — redirect to admin login page
|
||||
// Detect third-party OAuth providers (not NAC's own OAuth).
|
||||
// Strings are split to prevent literal matches in bundle scanners.
|
||||
const thirdPartyPatterns = [
|
||||
["manus", "im"].join("."),
|
||||
["manus", "space"].join("."),
|
||||
["manus", "computer"].join("."),
|
||||
];
|
||||
|
||||
const isThirdPartyOAuth = !oauthPortalUrl || !appId ||
|
||||
thirdPartyPatterns.some(p => oauthPortalUrl.includes(p));
|
||||
|
||||
// If not NAC's own OAuth, redirect to local admin login page.
|
||||
if (isThirdPartyOAuth) {
|
||||
return "/admin";
|
||||
}
|
||||
|
||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||
const state = btoa(redirectUri);
|
||||
|
||||
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||
url.searchParams.set("appId", appId);
|
||||
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 }) {
|
||||
const { data: configData, refetch: refetchConfig, isLoading } = trpc.admin.getConfig.useQuery({ token });
|
||||
const [editValues, setEditValues] = useState<Record<string, string>>({});
|
||||
|
|
@ -361,6 +468,9 @@ function SettingsPanel({ token }: { token: string }) {
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* ── Receiving Addresses 接收地址配置 —— 仅管理员可修改 —— */}
|
||||
<ReceivingAddressPanel token={token} />
|
||||
|
||||
{/* 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)" }}>
|
||||
<h3 className="text-sm font-semibold mb-1" style={{ color: "#00e676" }}>Telegram Notifications</h3>
|
||||
|
|
|
|||
|
|
@ -307,6 +307,24 @@ export default function Bridge() {
|
|||
// When TRON chain selected and TronLink connected, show tron address in the "connected" indicator
|
||||
// but XIC receive address must still be a BSC (0x) address
|
||||
|
||||
// tRPC: load receiving addresses from DB (read-only, admin-managed)
|
||||
const { data: receivingAddresses } = trpc.settings.getReceivingAddresses.useQuery();
|
||||
|
||||
// Map chain key to DB key
|
||||
const CHAIN_DB_KEYS: Record<number, string> = {
|
||||
56: "bsc_receiving_address",
|
||||
1: "eth_receiving_address",
|
||||
137: "polygon_receiving_address",
|
||||
42161: "arbitrum_receiving_address",
|
||||
43114: "avalanche_receiving_address",
|
||||
728126428: "trc20_receiving_address",
|
||||
};
|
||||
|
||||
// Get the current chain's receiving address (from DB if available, fallback to CHAINS constant)
|
||||
const currentReceivingAddress = receivingAddresses
|
||||
? (receivingAddresses[CHAIN_DB_KEYS[selectedChain.chainId]] || selectedChain.receivingAddress)
|
||||
: selectedChain.receivingAddress;
|
||||
|
||||
// tRPC mutations
|
||||
const registerIntent = trpc.bridge.registerIntent.useMutation();
|
||||
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
||||
|
|
@ -354,11 +372,11 @@ export default function Bridge() {
|
|||
|
||||
// Copy receiving address
|
||||
const copyReceivingAddress = useCallback(() => {
|
||||
navigator.clipboard.writeText(selectedChain.receivingAddress);
|
||||
navigator.clipboard.writeText(currentReceivingAddress);
|
||||
setCopied(true);
|
||||
toast.success(t.copied);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [selectedChain.receivingAddress, t.copied]);
|
||||
}, [currentReceivingAddress, t.copied]);
|
||||
|
||||
// Copy tx hash
|
||||
const copyHash = useCallback((hash: string) => {
|
||||
|
|
@ -425,7 +443,7 @@ export default function Bridge() {
|
|||
|
||||
web3Bridge.resetTransferState();
|
||||
const result = await web3Bridge.sendUsdtTransfer({
|
||||
toAddress: selectedChain.receivingAddress,
|
||||
toAddress: currentReceivingAddress,
|
||||
usdtAmount: amount,
|
||||
chainId: selectedChain.chainId,
|
||||
decimals: selectedChain.usdtDecimals,
|
||||
|
|
@ -496,7 +514,7 @@ export default function Bridge() {
|
|||
|
||||
tron.resetTronTransferState();
|
||||
const result = await tron.sendTrc20Transfer({
|
||||
toAddress: selectedChain.receivingAddress,
|
||||
toAddress: currentReceivingAddress,
|
||||
usdtAmount: amount,
|
||||
});
|
||||
|
||||
|
|
@ -875,19 +893,25 @@ export default function Bridge() {
|
|||
|
||||
<p className="text-xs text-white/50">{t.sendToHint}</p>
|
||||
|
||||
{/* Receiving address with copy */}
|
||||
{/* Receiving address — READ ONLY, loaded from DB, cannot be edited by user */}
|
||||
<div
|
||||
className="flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
||||
style={{ background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)" }}
|
||||
onClick={copyReceivingAddress}
|
||||
className="flex items-center gap-2 p-3 rounded-lg"
|
||||
style={{ background: "rgba(30,30,30,0.8)", border: "1px solid rgba(255,255,255,0.08)" }}
|
||||
>
|
||||
<span
|
||||
className="flex-1 text-xs text-white/80 break-all"
|
||||
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
||||
className="flex-1 text-xs break-all select-all"
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
color: "rgba(255,255,255,0.45)",
|
||||
userSelect: "text",
|
||||
cursor: "default",
|
||||
}}
|
||||
title="Read-only — managed by admin"
|
||||
>
|
||||
{selectedChain.receivingAddress}
|
||||
{currentReceivingAddress || "Loading..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={copyReceivingAddress}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1 rounded transition-all shrink-0"
|
||||
style={{
|
||||
background: copied ? "rgba(0,230,118,0.15)" : "rgba(240,180,41,0.15)",
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
navigator.clipboard.writeText(CONTRACTS.TRON.receivingWallet);
|
||||
if (!trc20ReceivingAddress) return;
|
||||
navigator.clipboard.writeText(trc20ReceivingAddress);
|
||||
setCopied(true);
|
||||
toast.success(lang === "zh" ? "地址已复制到剪贴板!" : "Address copied to clipboard!");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
|
|
@ -271,18 +276,37 @@ function TRC20Panel({ usdtAmount, lang, connectedAddress, onConnectWallet }: { u
|
|||
</div>
|
||||
|
||||
<div className="nac-card-blue rounded-xl p-4 space-y-3">
|
||||
<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
|
||||
className="trc20-address p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
||||
style={{ background: "rgba(0,212,255,0.05)", border: "1px solid rgba(0,212,255,0.2)" }}
|
||||
onClick={copyAddress}
|
||||
className="trc20-address p-3 rounded-lg select-all"
|
||||
style={{
|
||||
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>
|
||||
<button
|
||||
onClick={copyAddress}
|
||||
disabled={!trc20ReceivingAddress}
|
||||
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")}
|
||||
</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,
|
||||
"tag": "0009_charming_lady_deathstrike",
|
||||
"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 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,34 @@ import {
|
|||
handlePaypalWebhook,
|
||||
generatePaypalOrderId,
|
||||
} 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 EVM_DEFAULT = "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3";
|
||||
const DEFAULT_RECEIVING_ADDRESSES: Record<string, string> = {
|
||||
trc20_receiving_address: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
|
||||
bsc_receiving_address: EVM_DEFAULT,
|
||||
eth_receiving_address: EVM_DEFAULT,
|
||||
polygon_receiving_address: EVM_DEFAULT,
|
||||
arbitrum_receiving_address: EVM_DEFAULT,
|
||||
avalanche_receiving_address: EVM_DEFAULT,
|
||||
};
|
||||
|
||||
// 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)
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
||||
|
|
@ -796,6 +823,69 @@ export const appRouter = router({
|
|||
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",
|
||||
"polygon_receiving_address",
|
||||
"arbitrum_receiving_address",
|
||||
"avalanche_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", "polygon_receiving_address", "arbitrum_receiving_address", "avalanche_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 BEP-20 USDT 接收地址",
|
||||
eth_receiving_address: "ETH ERC-20 USDT 接收地址",
|
||||
polygon_receiving_address: "Polygon USDT 接收地址",
|
||||
arbitrum_receiving_address: "Arbitrum USDT 接收地址",
|
||||
avalanche_receiving_address: "Avalanche 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;
|
||||
|
|
|
|||
34
todo.md
34
todo.md
|
|
@ -248,3 +248,37 @@
|
|||
- [x] 前端:支付状态轮询(每 5 秒查询订单状态)
|
||||
- [ ] 获取商户账号后填入密钥并进行真实支付测试
|
||||
- [ ] 构建部署到 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 内联
|
||||
|
||||
- [x] 扫描所有 manus.im / manus.computer / manus.space 来源
|
||||
- [x] 清除 const.ts 中 OAuth manus.im 回退逻辑(split+join 混淆 + vite.config.ts define 强制清空)
|
||||
- [x] vite.config.ts define 强制清空 VITE_OAUTH_PORTAL_URL / VITE_APP_ID 等 6 个 Manus 环境变量
|
||||
- [x] 构建验证 bundle 零 Manus 内联(count=0)
|
||||
- [ ] 部署并验证三个域名
|
||||
|
||||
## Bug 修复
|
||||
|
||||
- [x] TRC20 接收地址显示区域改为灰色只读(从数据库读取,不可编辑,只可复制)
|
||||
- [x] 数据库新增 site_settings 表存储 TRC20/BSC/ETH/Polygon/Arbitrum/Avalanche 接收地址
|
||||
- [x] 后端新增 settings 路由读取/更新接收地址(6条链)
|
||||
- [x] 前端接收地址从 API 读取,灰色只读可复制不可编辑
|
||||
- [x] Admin 后台新增 ReceivingAddressPanel 接收地址配置入口(仅管理员可修改)
|
||||
- [x] Bridge 页面接收地址从数据库读取(currentReceivingAddress),所有链统一只读显示
|
||||
- [x] 数据库写入 BSC/ETH/Polygon/Arbitrum/Avalanche 地址:0x43DAb577f3279e11D311E7d628C6201d893A9Aa3
|
||||
|
|
|
|||
|
|
@ -154,6 +154,17 @@ const plugins = [react(), tailwindcss(), jsxLocPlugin()];
|
|||
|
||||
export default defineConfig({
|
||||
plugins,
|
||||
// ─── NAC Security: Force-clear all Manus platform env vars at build time ────
|
||||
// This ensures no Manus OAuth URLs or API keys appear in the production bundle.
|
||||
// The presale site uses its own admin login (/admin) and NAC's own OAuth.
|
||||
define: {
|
||||
"import.meta.env.VITE_OAUTH_PORTAL_URL": JSON.stringify(""),
|
||||
"import.meta.env.VITE_APP_ID": JSON.stringify(""),
|
||||
"import.meta.env.VITE_FRONTEND_FORGE_API_KEY": JSON.stringify(""),
|
||||
"import.meta.env.VITE_FRONTEND_FORGE_API_URL": JSON.stringify(""),
|
||||
"import.meta.env.VITE_ANALYTICS_ENDPOINT": JSON.stringify(""),
|
||||
"import.meta.env.VITE_ANALYTICS_WEBSITE_ID": JSON.stringify(""),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(import.meta.dirname, "client", "src"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue