diff --git a/app/[locale]/join/page.tsx b/app/[locale]/join/page.tsx index 7e59f9d..5f1b849 100644 --- a/app/[locale]/join/page.tsx +++ b/app/[locale]/join/page.tsx @@ -59,28 +59,28 @@ export default function JoinPage() { const [authOpen, setAuthOpen] = useState(false); const [pendingSubmit, setPendingSubmit] = useState(false); const [isAlreadyRecorded, setIsAlreadyRecorded] = useState(false); - const [checkingStatus, setCheckingStatus] = useState(true); useEffect(() => { if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("paid") === "1") { setSubmitted(true); - setCheckingStatus(false); return; } const checkExisting = async () => { - const meRes = await fetch(apiUrl("/api/auth/me"), { credentials: "include" }); - const meData = await meRes.json(); - if (!meData?.user) { - setCheckingStatus(false); - return; + try { + const meRes = await fetch(apiUrl("/api/auth/me"), { credentials: "include" }); + const meData = await meRes.json().catch(() => ({})); + if (!meRes.ok || !meData?.user) { + return; + } + const statusRes = await fetch(apiUrl("/api/join/status"), { credentials: "include", cache: "no-store" }); + const statusData = await statusRes.json().catch(() => ({})); + if (statusRes.ok && statusData?.hasRecord && statusData?.isComplete) { + setIsAlreadyRecorded(true); + setSubmitted(true); + } + } catch { + // Keep the form usable when the API proxy/backend is temporarily unavailable. } - const statusRes = await fetch(apiUrl("/api/join/status"), { credentials: "include", cache: "no-store" }); - const statusData = await statusRes.json(); - if (statusData?.hasRecord && statusData?.isComplete) { - setIsAlreadyRecorded(true); - setSubmitted(true); - } - setCheckingStatus(false); }; checkExisting(); }, []); @@ -200,14 +200,6 @@ export default function JoinPage() { ); } - if (checkingStatus) { - return ( -
- 请妥善保存以下默认密码,用于登录本网站(游牧中国) -
-
- {DEFAULT_PASSWORD}
-
-
- 正在跳转到支付页面...
${pendingOrderScript}`; -} - -function buildQrHtml( - imgSrc: string, - returnUrl: string, - orderId: string, - opts?: { channel?: string; successDesc?: string; confirmUrl?: string } -): string { - const ch = (opts?.channel || "wxpay").toLowerCase(); - const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat"; - const title = isWx ? "微信扫码支付" : "支付宝扫码支付"; - const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付"; - const successDesc = opts?.successDesc || "支付成功,即将跳转"; - const confirmUrl = opts?.confirmUrl || "/api/pay/confirm"; - const esc = (s: string) => - s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c); - return ` - -${esc(hint)}
-等待支付中...
-- {mode === "login" ? t("loginHint") : t("registerHint")} + {t("loginHint")}
-- {mode === "login" ? ( - <> - {t("noAccount")}{" "} - - > - ) : ( - <> - {t("hasAccount")}{" "} - - > - )} -
+ {GOOGLE_CLIENT_ID && ( + <> +正在跳转到支付页面...
+ +""" + + +def build_redirect_html(pay_url: str, order_id: str) -> str: + order_value = escape(f"{order_id}|{int(datetime.now().timestamp() * 1000)}", quote=True) + return f"""正在跳转到支付页面...
+""" + + +def build_qr_html(img_src: str, return_url: str, order_id: str) -> str: + order_value = escape(f"{order_id}|{int(datetime.now().timestamp() * 1000)}", quote=True) + return f"""请使用微信扫描二维码完成支付
订单 {order_id} 已自动标记为支付成功。
-3 秒后返回站点完成会员开通。
- -立即返回""" - ) - html_response.set_cookie("join_pay_order", cookie_value, path="/", max_age=900) + set_pay_order_cookie(response, order_id) + + if settings.dev_auto_pay: + redirect_url = return_url + html_response = HTMLResponse(build_redirect_html(redirect_url, order_id)) + set_pay_order_cookie(html_response, order_id) + return html_response if want_html else {"ok": True, "order_id": order_id, "paid": True} + + amount_yuan = f"{total_fee / 100:.2f}" + provider = provider_for(device, requested_provider, request.headers.get("user-agent", "")) + notify_url = f"{settings.base_url}/api/pay/{provider.name}_notify" + name = payment_name(pay_type) + client_ip = client_ip_from_request(request) + + if isinstance(provider, XorPayProvider): + created = provider.create_order(order_id=order_id, name=name, amount_yuan=amount_yuan, notify_url=notify_url) + if not want_html: + return {"ok": True, "order_id": order_id, "provider": provider.name, "params": created["params"], "payUrl": created["pay_url"]} + html_response = HTMLResponse(build_auto_submit_html(created["pay_url"], created["params"], order_id)) + set_pay_order_cookie(html_response, order_id) return html_response - return {"ok": True, "order_id": order_id, "paid": settings.dev_auto_pay} + + api_result = await provider.create_order_api( + order_id=order_id, + name=name, + amount_yuan=amount_yuan, + channel=channel, + notify_url=notify_url, + return_url=return_url, + client_ip=client_ip, + device=device, + ) + if api_result.get("status") == "ok": + if not want_html: + return {"ok": True, "order_id": order_id, "provider": provider.name, **api_result} + if device == "pc" and api_result.get("img"): + html_response = HTMLResponse(build_qr_html(str(api_result["img"]), return_url, order_id)) + set_pay_order_cookie(html_response, order_id) + return html_response + pay_url = api_result.get("payurl2") if device == "h5" and api_result.get("payurl2") else api_result.get("payurl") + if pay_url: + html_response = HTMLResponse(build_redirect_html(str(pay_url), order_id)) + set_pay_order_cookie(html_response, order_id) + return html_response + + created = provider.create_order( + order_id=order_id, + name=name, + amount_yuan=amount_yuan, + channel=channel, + notify_url=notify_url, + return_url=return_url, + client_ip=client_ip, + ) + if not want_html: + return {"ok": True, "order_id": order_id, "provider": provider.name, "params": created["params"], "payUrl": created["pay_url"]} + html_response = HTMLResponse(build_auto_submit_html(created["pay_url"], created["params"], order_id)) + set_pay_order_cookie(html_response, order_id) + return html_response @app.get("/api/pay/status") async def pay_status(order_id: str) -> dict[str, Any]: - order = await pb.first_record("orders", filter=f'orderId={pb_quote(order_id)}') - return {"ok": True, "paid": bool(order and order.get("status") == "paid"), "order": order} + order = await find_order(order_id) + if order and order.get("status") == "paid": + return {"ok": True, "paid": True, "order": order} + paid, pay_price = await query_gateway_paid(order_id) + if paid: + order = await mark_order_paid(order_id, pay_price) + return {"ok": True, "paid": paid, "order": order} + + +@app.get("/cnomadcna/order_status") +async def cnomadcna_order_status(order_id: str = "", out_trade_no: str = "") -> dict[str, Any]: + target = (order_id or out_trade_no).strip() + if not target: + return {"paid": False, "error": "missing order_id"} + result = await pay_status(target) + return {"paid": bool(result.get("paid")), "order": result.get("order")} + + +@app.get("/cnomadcna/zpay_order_status") +async def cnomadcna_zpay_order_status(out_trade_no: str) -> dict[str, Any]: + return await ZPayProvider().query_order_status(out_trade_no) + + +@app.api_route("/api/pay/zpay_notify", methods=["GET", "POST"]) +@app.api_route("/cnomadcna/zpay_notify", methods=["GET", "POST"]) +async def zpay_notify(request: Request) -> FastAPIResponse: + if request.method == "GET": + data = dict(request.query_params) + else: + form = await request.form() + data = dict(form) + provider = ZPayProvider() + order_id = str(data.get("out_trade_no", "")) + if order_id in processed_orders: + return FastAPIResponse(provider.success_response(), media_type="text/plain") + if not provider.verify_notify(data): + raise HTTPException(status_code=400, detail="sign error") + parsed = provider.parse_notify(data) + if parsed.get("trade_status") == "success": + await finalize_paid_order(parsed.get("order_id", ""), parsed.get("pay_price", "")) + return FastAPIResponse(provider.success_response(), media_type="text/plain") + + +@app.post("/api/pay/xorpay_notify") +@app.post("/cnomadcna/xorpay_notify") +async def xorpay_notify(request: Request) -> FastAPIResponse: + form = await request.form() + data = dict(form) + provider = XorPayProvider() + order_id = str(data.get("order_id", "")) + if order_id in processed_orders: + return FastAPIResponse(provider.success_response(), media_type="text/plain") + if not provider.verify_notify(data): + raise HTTPException(status_code=400, detail="sign error") + parsed = provider.parse_notify(data) + await finalize_paid_order(parsed.get("order_id", ""), parsed.get("pay_price", "")) + return FastAPIResponse(provider.success_response(), media_type="text/plain") @app.get("/api/cities") diff --git a/backend/payment.py b/backend/payment.py new file mode 100644 index 0000000..b0ffb08 --- /dev/null +++ b/backend/payment.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +import hashlib +import json +from typing import Any + +import httpx + +from .settings import get_settings + + +def _md5(value: str) -> str: + return hashlib.md5(value.encode("utf-8")).hexdigest().lower() + + +def _is_internal_ip(ip: str | None) -> bool: + if not ip: + return True + value = ip.strip().lower() + if value in {"127.0.0.1", "localhost", "::1"}: + return True + if value.startswith("10.") or value.startswith("192.168."): + return True + if value.startswith("172."): + parts = value.split(".") + if len(parts) > 1 and parts[1].isdigit(): + return 16 <= int(parts[1]) <= 31 + return False + + +def resolve_channel(channel: str | None) -> str: + return "wxpay" if str(channel or "").lower() in {"", "wx", "wechat", "wxpay"} else "alipay" + + +class ZPayProvider: + name = "zpay" + + def __init__(self) -> None: + settings = get_settings() + self.pid = settings.zpay_pid + self.key = settings.zpay_key + self.submit_url = settings.zpay_submit_url + self.mapi_url = settings.zpay_mapi_url + self.query_url = settings.zpay_query_url + + def sign(self, params: dict[str, Any]) -> str: + filtered = { + key: value + for key, value in params.items() + if key not in {"sign", "sign_type"} and value is not None and str(value) != "" + } + raw = "&".join(f"{key}={filtered[key]}" for key in sorted(filtered)) + self.key + return _md5(raw) + + def create_order( + self, + *, + order_id: str, + name: str, + amount_yuan: str, + channel: str, + notify_url: str, + return_url: str, + client_ip: str = "", + ) -> dict[str, Any]: + params: dict[str, Any] = { + "pid": self.pid, + "type": resolve_channel(channel), + "out_trade_no": order_id, + "notify_url": notify_url, + "return_url": return_url, + "name": name, + "money": amount_yuan, + "sign_type": "MD5", + } + if client_ip and not _is_internal_ip(client_ip): + params["clientip"] = client_ip + params["sign"] = self.sign(params) + return { + "status": "ok", + "provider": self.name, + "params": params, + "pay_url": self.submit_url, + "submit_method": "POST", + } + + async def create_order_api( + self, + *, + order_id: str, + name: str, + amount_yuan: str, + channel: str, + notify_url: str, + return_url: str, + client_ip: str = "", + device: str = "pc", + ) -> dict[str, Any]: + params: dict[str, Any] = { + "pid": self.pid, + "type": resolve_channel(channel), + "out_trade_no": order_id, + "notify_url": notify_url, + "return_url": return_url, + "name": name, + "money": amount_yuan, + "device": device, + "sign_type": "MD5", + } + if client_ip and not _is_internal_ip(client_ip): + params["clientip"] = client_ip + params["sign"] = self.sign(params) + + try: + async with httpx.AsyncClient(timeout=15.0, trust_env=False) as client: + res = await client.post(self.mapi_url, data=params) + except Exception as exc: + return {"status": "error", "provider": self.name, "msg": str(exc)} + + result: Any + try: + result = res.json() + except Exception: + result = {} + if isinstance(result, str): + try: + result = json.loads(result) + except Exception: + result = {} + if not isinstance(result, dict): + result = {} + msg = result.get("msg") + if isinstance(msg, str) and msg.strip().startswith("{"): + try: + inner = json.loads(msg) + if isinstance(inner, dict): + result = inner + except Exception: + pass + try: + code = int(float(result.get("code", 0))) + except (TypeError, ValueError): + code = 0 + if code == 1: + return { + "status": "ok", + "provider": self.name, + "payurl": result.get("payurl"), + "payurl2": result.get("payurl2"), + "qrcode": result.get("qrcode"), + "img": result.get("img"), + "trade_no": result.get("trade_no"), + } + return {"status": "error", "provider": self.name, "msg": result.get("msg") or res.text[:500]} + + def verify_notify(self, data: dict[str, Any]) -> bool: + return self.sign(data) == str(data.get("sign", "")) + + def parse_notify(self, data: dict[str, Any]) -> dict[str, Any]: + return { + "order_id": str(data.get("out_trade_no", "")), + "trade_no": str(data.get("trade_no", "")), + "pay_price": str(data.get("money", "")), + "trade_status": "success" if data.get("trade_status") == "TRADE_SUCCESS" else "pending", + } + + async def query_order_status(self, order_id: str, timeout: float = 8.0) -> dict[str, Any]: + try: + async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client: + res = await client.get( + self.query_url, + params={ + "act": "order", + "pid": self.pid, + "key": self.key, + "out_trade_no": order_id, + }, + ) + data = res.json() if res.is_success else {} + except Exception as exc: + return {"paid": False, "error": str(exc)} + try: + code = int(float(data.get("code", 0))) + status = int(float(data.get("status", 0))) + except (TypeError, ValueError): + return {"paid": False, "msg": data.get("msg", "查询失败")} + pay_price = data.get("money") or data.get("pay_price") or "" + return {"paid": code == 1 and status == 1, "status": status, "pay_price": str(pay_price)} + + def success_response(self) -> str: + return "success" + + +class XorPayProvider: + name = "xorpay" + + def __init__(self) -> None: + settings = get_settings() + self.aid = settings.xorpay_aid + self.secret = settings.xorpay_secret + self.cashier_url = settings.xorpay_cashier_url.rstrip("/") + self.query_url = settings.xorpay_query_url.rstrip("/") + + def create_order( + self, + *, + order_id: str, + name: str, + amount_yuan: str, + notify_url: str, + ) -> dict[str, Any]: + params = { + "name": name, + "pay_type": "jsapi", + "price": amount_yuan, + "order_id": order_id, + "notify_url": notify_url, + } + params["sign"] = _md5( + params["name"] + params["pay_type"] + params["price"] + params["order_id"] + params["notify_url"] + self.secret + ) + return {"status": "ok", "provider": self.name, "params": params, "pay_url": f"{self.cashier_url}/{self.aid}"} + + def verify_notify(self, data: dict[str, Any]) -> bool: + raw = ( + str(data.get("aoid", "")) + + str(data.get("order_id", "")) + + str(data.get("pay_price", "")) + + str(data.get("pay_time", "")) + + self.secret + ) + return _md5(raw) == str(data.get("sign", "")) + + def parse_notify(self, data: dict[str, Any]) -> dict[str, Any]: + return { + "order_id": str(data.get("order_id", "")), + "trade_no": str(data.get("aoid", "")), + "pay_price": str(data.get("pay_price", "")), + "trade_status": "success", + } + + async def query_order_status(self, order_id: str, timeout: float = 8.0) -> dict[str, Any]: + sign = _md5(order_id + self.secret) + try: + async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client: + res = await client.get(f"{self.query_url}/{self.aid}", params={"order_id": order_id, "sign": sign}) + data = res.json() if res.is_success else {} + status = str(data.get("status", "")).lower() + return { + "paid": status in {"payed", "success"}, + "status": status, + "pay_price": data.get("pay_price") or data.get("price") or "", + } + except Exception as exc: + return {"paid": False, "error": str(exc)} + + def success_response(self) -> str: + return "ok" + + +def provider_for(device: str = "pc", requested: str = "", user_agent: str = "") -> ZPayProvider | XorPayProvider: + req = requested.strip().lower() + if req == "xorpay": + return XorPayProvider() + if req == "zpay": + return ZPayProvider() + if device.lower() == "wechat" or "micromessenger" in user_agent.lower(): + return XorPayProvider() + return ZPayProvider() diff --git a/backend/settings.py b/backend/settings.py index dff6961..1a9ce55 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -5,15 +5,68 @@ from functools import lru_cache from pathlib import Path +ROOT_DIR = Path(__file__).resolve().parent.parent + + +def _read_env_file(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.exists(): + return values + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + if key: + values[key] = value + return values + + +def _load_env_files() -> None: + loaded: dict[str, str] = {} + for path in [ + ROOT_DIR / ".env", + ROOT_DIR / ".env.local", + ROOT_DIR / "backend" / ".env", + ROOT_DIR / "backend" / ".env.local", + ]: + loaded.update(_read_env_file(path)) + for key, value in loaded.items(): + os.environ.setdefault(key, value) + + +_load_env_files() + + def _split_csv(value: str) -> list[str]: return [item.strip() for item in value.split(",") if item.strip()] +def _bool_env(name: str, default: str = "false") -> bool: + return os.getenv(name, default).lower() in {"1", "true", "yes", "on"} + + class Settings: def __init__(self) -> None: - self.pb_url = os.getenv("PB_URL", "http://127.0.0.1:8090").rstrip("/") - self.pb_admin_email = os.getenv("PB_ADMIN_EMAIL", "admin@nomadcna.local") - self.pb_admin_password = os.getenv("PB_ADMIN_PASSWORD", "NomadCNA123456") + self.pb_url = ( + os.getenv("PB_INTERNAL_URL") + or os.getenv("POCKETBASE_INTERNAL_URL") + or os.getenv("PB_URL") + or os.getenv("POCKETBASE_URL") + or "http://127.0.0.1:8090" + ).rstrip("/") + self.pb_admin_email = ( + os.getenv("PB_ADMIN_EMAIL") + or os.getenv("POCKETBASE_EMAIL") + or "admin@nomadcna.local" + ) + self.pb_admin_password = ( + os.getenv("PB_ADMIN_PASSWORD") + or os.getenv("POCKETBASE_PASSWORD") + or "NomadCNA123456" + ) self.cookie_name = os.getenv("API_COOKIE_NAME", "pb_session") self.frontend_origins = _split_csv( os.getenv( @@ -22,7 +75,38 @@ class Settings: ) ) self.upload_dir = Path(os.getenv("UPLOAD_DIR", "backend/uploads")) - self.dev_auto_pay = os.getenv("DEV_AUTO_PAY", "true").lower() == "true" + self.dev_auto_pay = _bool_env("DEV_AUTO_PAY", "true") + self.site_id = os.getenv("SITE_ID") or os.getenv("NEXT_PUBLIC_SITE_ID") or "meetup" + self.base_url = ( + os.getenv("API_PUBLIC_BASE_URL") + or os.getenv("BASE_URL") + or os.getenv("PAYMENT_API_URL") + or os.getenv("NEXT_PUBLIC_API_BASE_URL") + or "http://127.0.0.1:8000" + ).rstrip("/") + if self.base_url == "/api": + self.base_url = (os.getenv("NEXT_PUBLIC_SITE_URL") or "http://127.0.0.1:3001").rstrip("/") + self.upload_public_base_url = ( + os.getenv("UPLOAD_PUBLIC_BASE_URL") + or os.getenv("API_PUBLIC_BASE_URL") + or os.getenv("BASE_URL") + or self.base_url + ).rstrip("/") + self.email_auth_password = os.getenv("EMAIL_AUTH_PASSWORD", "NomadCNAEmailLogin2026!") + self.google_client_id = os.getenv("GOOGLE_CLIENT_ID") or os.getenv("NEXT_PUBLIC_GOOGLE_CLIENT_ID") or "" + self.payment_provider = os.getenv("PAYMENT_PROVIDER", "zpay").lower() + self.payment_default_amount = int(os.getenv("PAYMENT_DEFAULT_AMOUNT", "100") or "100") + self.payment_join_amount = int(os.getenv("PAYMENT_JOIN_AMOUNT", str(self.payment_default_amount)) or str(self.payment_default_amount)) + self.payment_join_type = os.getenv("PAYMENT_JOIN_TYPE", "meetup") + self.zpay_pid = os.getenv("ZPAY_PID", "2025121809351743") + self.zpay_key = os.getenv("ZPAY_KEY", "tpEi7wWIWI2kXiYVTpIG6j7it0mjVW89") + self.zpay_submit_url = os.getenv("ZPAY_SUBMIT_URL", "https://zpayz.cn/submit.php") + self.zpay_mapi_url = os.getenv("ZPAY_MAPI_URL", "https://zpayz.cn/mapi.php") + self.zpay_query_url = os.getenv("ZPAY_QUERY_URL", "https://zpayz.cn/api.php") + self.xorpay_aid = os.getenv("XORPAY_AID", "8220") + self.xorpay_secret = os.getenv("XORPAY_SECRET", "afcacd99570945f88de62624aaa3578e") + self.xorpay_cashier_url = os.getenv("XORPAY_CASHIER_URL", "https://xorpay.com/api/cashier") + self.xorpay_query_url = os.getenv("XORPAY_QUERY_URL", "https://xorpay.com/api/query2") @lru_cache(maxsize=1) diff --git a/config/domain.config.ts b/config/domain.config.ts deleted file mode 100644 index db7b53b..0000000 --- a/config/domain.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 域名与根域配置 - 特色标识 - * 更换域名(如 *.hackrobot.cn -> *.nomadro.com)时,只需设置 ROOT_DOMAIN 或各独立变量 - */ - -const isDev = process.env.NODE_ENV === "development"; - -export const ROOT_DOMAIN = - process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn"; - -/** 支付 API 默认地址。生产环境固定为 api 域名,避免 .env 中 127.0.0.1 导致支付失败 */ -export const DEFAULT_PAYMENT_API_URL = isDev - ? "http://127.0.0.1:8007" - : `https://api.${ROOT_DOMAIN}`; - -/** 生产环境若 PAYMENT_API_URL 指向 localhost,则忽略并使用 api 域名 */ -export function resolvePaymentApiUrl(): string { - const url = process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL; - if (!isDev && /^(https?:\/\/)?(127\.0\.0\.1|localhost)(:\d+)?(\/|$)/i.test(url)) { - return `https://api.${ROOT_DOMAIN}`; - } - return url; -} - -export const DEFAULT_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`; - -export const DEFAULT_MINIO_HOST = `minioweb.${ROOT_DOMAIN}`; - -/** digital 专题站默认地址(用于跨站链接) */ -export const DEFAULT_DIGITAL_URL = `https://digital.${ROOT_DOMAIN}`; diff --git a/config/index.ts b/config/index.ts index a8ff674..d113ea2 100644 --- a/config/index.ts +++ b/config/index.ts @@ -4,14 +4,10 @@ export { getHomeConfig, HOME_CONFIG, MEMBER_PHOTOS, TRAVELING_NOW, MEETUPS, HOT_ export { FILTER_OPTIONS, FILTER_LABELS, type FilterOption } from "./filter.config"; export { FOOTER_SECTIONS, getFooterSections, type FooterLink, type FooterSection } from "./footer.config"; export { - getPocketBaseConfig, getPaymentConfig, getJoinPaymentConfig, - getMinioConfig, SITE_ID, } from "./services.config"; export type { - PocketBaseConfig, PaymentConfig, - MinioConfig, } from "./services.config"; diff --git a/config/services.config.ts b/config/services.config.ts index 8268d16..67c4522 100644 --- a/config/services.config.ts +++ b/config/services.config.ts @@ -1,80 +1,23 @@ /** - * 服务配置 - PocketBase、支付、MinIO - * 域名更换:设置 ROOT_DOMAIN(如 nomadro.com)可推导默认 API 地址 + * 前端服务配置。 + * 浏览器侧只保留 UI 需要的公开配置;后端地址、支付网关、存储和 PB 连接都由 FastAPI 持有。 */ -import { - DEFAULT_PAYMENT_API_URL, - DEFAULT_POCKETBASE_URL, - DEFAULT_MINIO_HOST, - resolvePaymentApiUrl, -} from "./domain.config"; - -export interface PocketBaseConfig { - enabled: boolean; - url: string; - usersCollection: string; - joinCollection: string; -} - export interface PaymentConfig { - enabled: boolean; - apiUrl: string; - provider: "zpay" | "xorpay"; defaultAmount: number; defaultType: string; - zpaySubmitUrl: string; } -export interface MinioConfig { - enabled: boolean; - endPoint: string; - port: number; - useSSL: boolean; - bucket: string; - accessKey: string; - secretKey: string; - region: string; - publicUrl: string; - uploadPrefix: string; -} - -const isDev = process.env.NODE_ENV === "development"; -const PAYJSAPI_PORT = process.env.PAYJSAPI_PORT || "8007"; - /** 站点标识,用于 VIP 按站点隔离 */ export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "meetup"; -/** meetup 加入游牧中国 价格(分),88元=8800 */ +/** meetup 加入游牧中国价格(分),88 元 = 8800 */ export const MEETUP_JOIN_AMOUNT = 8800; -export function getApiUrlFromRequest(hostHeader: string | null): string | null { - if (!hostHeader) return null; - if (!isDev) return resolvePaymentApiUrl(); - const hostname = hostHeader.split(":")[0]; - return `http://${hostname}:${PAYJSAPI_PORT}`; -} - -export function getPocketBaseConfig(): PocketBaseConfig { - return { - enabled: true, - url: - process.env.NEXT_PUBLIC_POCKETBASE_URL || - process.env.POCKETBASE_URL || - DEFAULT_POCKETBASE_URL, - usersCollection: "users", - joinCollection: process.env.POCKETBASE_JOIN_COLLECTION || "solan", - }; -} - export function getPaymentConfig(): PaymentConfig { return { - enabled: true, - apiUrl: resolvePaymentApiUrl(), - provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay", defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 8800, defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup", - zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php", }; } @@ -85,29 +28,3 @@ export function getJoinPaymentConfig(): { amount: number; type: string } { type: process.env.PAYMENT_JOIN_TYPE || "meetup", }; } - -export function getMinioConfig(themeOverride?: { - bucket?: string; - uploadPrefix?: string; -}): MinioConfig { - const endPoint = process.env.MINIO_ENDPOINT || DEFAULT_MINIO_HOST; - const port = parseInt(process.env.MINIO_PORT || "9035", 10); - const bucket = themeOverride?.bucket || process.env.MINIO_BUCKET || "hackrobot"; - const publicUrl = - process.env.MINIO_PUBLIC_URL || `https://${endPoint}/${bucket}`; - const uploadPrefix = - themeOverride?.uploadPrefix || process.env.MINIO_UPLOAD_PREFIX || "joins"; - - return { - enabled: true, - endPoint, - port, - useSSL: process.env.MINIO_USE_SSL === "true", - bucket, - accessKey: process.env.MINIO_ACCESS_KEY || "", - secretKey: process.env.MINIO_SECRET_KEY || "", - region: process.env.MINIO_REGION || "china", - publicUrl: publicUrl.replace(/\/$/, ""), - uploadPrefix, - }; -} diff --git a/messages/en.json b/messages/en.json index 3081258..ee77ce3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5,7 +5,7 @@ "siteNameShort": "NomadCNA", "currency": "CNY ¥", "logout": "Logout", - "loginRegister": "Login / Sign up", + "loginRegister": "Email / Google login", "explore": "Explore →", "toggleMenu": "Toggle menu", "themeLight": "Switch to dark mode", @@ -308,22 +308,12 @@ "submitAnother": "Submit another" }, "auth": { - "title": "Login / Sign up", + "title": "Email / Google login", "login": "Login", - "register": "Register", "email": "Email", - "password": "Password", - "passwordConfirm": "Confirm Password", "submit": "Login", "close": "Close", - "loginHint": "Login with email and password", - "registerHint": "Create new account", - "noAccount": "No account yet?", - "hasAccount": "Already have an account?", - "signUpNow": "Sign up now", - "goLogin": "Go to login", - "passwordMismatch": "Passwords do not match", - "passwordMinLength": "Password must be at least 8 characters" + "loginHint": "Enter your email or continue with Google" }, "pricing": { "title": "Membership", diff --git a/messages/zh.json b/messages/zh.json index 556acbf..6146d16 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -5,7 +5,7 @@ "siteNameShort": "游牧中国", "currency": "CNY ¥", "logout": "退出", - "loginRegister": "登录 / 注册", + "loginRegister": "邮箱 / Google 登录", "explore": "开始探索 →", "toggleMenu": "切换菜单", "themeLight": "切换到夜间模式", @@ -308,22 +308,12 @@ "submitAnother": "再提交一条" }, "auth": { - "title": "登录 / 注册", + "title": "邮箱 / Google 登录", "login": "登录", - "register": "注册", "email": "邮箱", - "password": "密码", - "passwordConfirm": "确认密码", "submit": "登录", "close": "关闭", - "loginHint": "使用邮箱密码登录", - "registerHint": "创建新账号", - "noAccount": "还没有账号?", - "hasAccount": "已有账号?", - "signUpNow": "立即注册", - "goLogin": "去登录", - "passwordMismatch": "两次密码不一致", - "passwordMinLength": "密码至少 8 位" + "loginHint": "输入邮箱登录,或使用 Google 继续" }, "pricing": { "title": "会员体系", diff --git a/next.config.ts b/next.config.ts index d247f8f..316716c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,14 +1,22 @@ import type { NextConfig } from "next"; +const fastApiTarget = ( + process.env.FASTAPI_PROXY_TARGET || + process.env.BACKEND_API_BASE_URL || + process.env.API_PROXY_TARGET || + "http://127.0.0.1:8000" +).replace(/\/$/, ""); + const nextConfig: NextConfig = { async rewrites() { - return [ - { - source: - "/api/:path((?:health|cities|content|notifications|recommendations|routes|services|downloads|meetups|discussions|gigs|profiles|matches|feedback|stats|ai).*)", - destination: "http://127.0.0.1:8000/api/:path", - }, - ]; + return { + beforeFiles: [ + { + source: "/api/:path*", + destination: `${fastApiTarget}/api/:path*`, + }, + ], + }; }, allowedDevOrigins: [ "localhost", diff --git a/package.json b/package.json index 54026d1..e44caa7 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,7 @@ }, "dependencies": { "echarts": "^6.0.0", - "minio": "^8.0.7", "next": "16.1.6", - "pocketbase": "^0.26.8", "react": "19.2.3", "react-dom": "19.2.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63de4a9..12fb0b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,9 @@ importers: echarts: specifier: ^6.0.0 version: 6.0.0 - minio: - specifier: ^8.0.7 - version: 8.0.7 next: specifier: 16.1.6 version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - pocketbase: - specifier: ^0.26.8 - version: 0.26.8 react: specifier: 19.2.3 version: 19.2.3 @@ -238,6 +232,7 @@ packages: resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} @@ -748,9 +743,6 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -775,9 +767,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - block-stream2@2.1.0: - resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -789,18 +778,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browser-or-node@2.1.1: - resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -879,10 +861,6 @@ packages: supports-color: optional: true - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1079,9 +1057,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1095,13 +1070,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-xml-builder@1.0.0: - resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==} - - fast-xml-parser@5.4.2: - resolution: {integrity: sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==} - hasBin: true - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1122,10 +1090,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - filter-obj@1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1250,17 +1214,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ipaddr.js@2.3.0: - resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} - engines: {node: '>= 10'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -1503,9 +1460,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1528,14 +1482,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -1546,10 +1492,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minio@8.0.7: - resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==} - engines: {node: ^16 || ^18 || >=20} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1668,9 +1610,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pocketbase@0.26.8: - resolution: {integrity: sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low==} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -1694,10 +1633,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - query-string@7.1.3: - resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} - engines: {node: '>=6'} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1713,10 +1648,6 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -1753,9 +1684,6 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -1764,10 +1692,6 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sax@1.5.0: - resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} - engines: {node: '>=11.0.0'} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1824,10 +1748,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - split-on-first@1.1.0: - resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} - engines: {node: '>=6'} - stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -1835,16 +1755,6 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stream-chain@2.2.5: - resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} - - stream-json@1.9.1: - resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} - - strict-uri-encode@2.0.0: - resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} - engines: {node: '>=4'} - string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -1868,9 +1778,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -1879,9 +1786,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strnum@2.2.0: - resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -1910,9 +1814,6 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -1987,9 +1888,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2015,14 +1913,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - xml2js@0.6.2: - resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2726,8 +2616,6 @@ snapshots: async-function@1.0.0: {} - async@3.2.6: {} - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -2742,10 +2630,6 @@ snapshots: baseline-browser-mapping@2.10.0: {} - block-stream2@2.1.0: - dependencies: - readable-stream: 3.6.2 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2759,8 +2643,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browser-or-node@2.1.1: {} - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.0 @@ -2769,8 +2651,6 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer-crc32@1.0.0: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2845,8 +2725,6 @@ snapshots: dependencies: ms: 2.1.3 - decode-uri-component@0.2.2: {} - deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3197,8 +3075,6 @@ snapshots: esutils@2.0.3: {} - eventemitter3@5.0.4: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3213,13 +3089,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-xml-builder@1.0.0: {} - - fast-xml-parser@5.4.2: - dependencies: - fast-xml-builder: 1.0.0 - strnum: 2.2.0 - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3236,8 +3105,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - filter-obj@1.1.0: {} - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3359,16 +3226,12 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - internal-slot@1.1.0: dependencies: es-errors: 1.3.0 hasown: 2.0.2 side-channel: 1.1.0 - ipaddr.js@2.3.0: {} - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -3593,8 +3456,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash@4.17.23: {} - loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3616,12 +3477,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -3632,22 +3487,6 @@ snapshots: minimist@1.2.8: {} - minio@8.0.7: - dependencies: - async: 3.2.6 - block-stream2: 2.1.0 - browser-or-node: 2.1.1 - buffer-crc32: 1.0.0 - eventemitter3: 5.0.4 - fast-xml-parser: 5.4.2 - ipaddr.js: 2.3.0 - lodash: 4.17.23 - mime-types: 2.1.35 - query-string: 7.1.3 - stream-json: 1.9.1 - through2: 4.0.2 - xml2js: 0.6.2 - ms@2.1.3: {} nanoid@3.3.11: {} @@ -3770,8 +3609,6 @@ snapshots: picomatch@4.0.3: {} - pocketbase@0.26.8: {} - possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -3796,13 +3633,6 @@ snapshots: punycode@2.3.1: {} - query-string@7.1.3: - dependencies: - decode-uri-component: 0.2.2 - filter-obj: 1.1.0 - split-on-first: 1.1.0 - strict-uri-encode: 2.0.0 - queue-microtask@1.2.3: {} react-dom@19.2.3(react@19.2.3): @@ -3814,12 +3644,6 @@ snapshots: react@19.2.3: {} - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -3873,8 +3697,6 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -3886,8 +3708,6 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - sax@1.5.0: {} - scheduler@0.27.0: {} semver@6.3.1: {} @@ -3984,8 +3804,6 @@ snapshots: source-map-js@1.2.1: {} - split-on-first@1.1.0: {} - stable-hash@0.0.5: {} stop-iteration-iterator@1.1.0: @@ -3993,14 +3811,6 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stream-chain@2.2.5: {} - - stream-json@1.9.1: - dependencies: - stream-chain: 2.2.5 - - strict-uri-encode@2.0.0: {} - string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -4051,16 +3861,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} - strnum@2.2.0: {} - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): dependencies: client-only: 0.0.1 @@ -4078,10 +3882,6 @@ snapshots: tapable@2.3.0: {} - through2@4.0.2: - dependencies: - readable-stream: 3.6.2 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -4199,8 +3999,6 @@ snapshots: dependencies: punycode: 2.3.1 - util-deprecate@1.0.2: {} - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4248,13 +4046,6 @@ snapshots: word-wrap@1.2.5: {} - xml2js@0.6.2: - dependencies: - sax: 1.5.0 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - yallist@3.1.1: {} yocto-queue@0.1.0: {}