Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88aa96a2a1 | ||
|
|
9d593c77bf | ||
|
|
15dba690d3 |
30
.env.example
Normal file
30
.env.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# ========== 与 payjsapi/digital/nomadvip 同源 ==========
|
||||||
|
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
|
||||||
|
POCKETBASE_JOIN_COLLECTION=solan
|
||||||
|
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
POCKETBASE_PASSWORD=Xiao4669805@
|
||||||
|
|
||||||
|
# ZPay 支付 (payjsapi)
|
||||||
|
PAYMENT_API_URL=http://127.0.0.1:8007
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
PAYMENT_DEFAULT_AMOUNT=80
|
||||||
|
PAYMENT_DEFAULT_TYPE=meetup
|
||||||
|
PAYMENT_JOIN_AMOUNT=80
|
||||||
|
PAYMENT_JOIN_TYPE=meetup
|
||||||
|
ZPAY_SUBMIT_URL=https://zpayz.cn/submit.php
|
||||||
|
|
||||||
|
# MinIO 存储
|
||||||
|
MINIO_ENDPOINT=minioweb.hackrobot.cn
|
||||||
|
MINIO_PORT=9035
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_BUCKET=hackrobot
|
||||||
|
MINIO_ACCESS_KEY=
|
||||||
|
MINIO_SECRET_KEY=
|
||||||
|
MINIO_REGION=china
|
||||||
|
MINIO_UPLOAD_PREFIX=joins
|
||||||
|
|
||||||
|
# 站点(本地调试可改为 http://192.168.41.222:3001)
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# 生产环境跨站 SSO(本地不设置)
|
||||||
|
# AUTH_COOKIE_DOMAIN=.hackrobot.cn
|
||||||
12
.env.local
Normal file
12
.env.local
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 本地调试 - 支付测试 1 元、zpay 渠道
|
||||||
|
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
|
||||||
|
PAYMENT_API_URL=http://127.0.0.1:8007
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
PAYMENT_DEFAULT_AMOUNT=100
|
||||||
|
PAYMENT_JOIN_AMOUNT=100
|
||||||
|
NEXT_PUBLIC_SITE_ID=meetup
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# PocketBase 管理员(check-user、ensure-user、vip 需要)
|
||||||
|
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
POCKETBASE_PASSWORD=Xiao4669805@
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,7 +31,7 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
# .env*
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -84,12 +84,13 @@ export default function JoinPage() {
|
|||||||
checkExisting();
|
checkExisting();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 轮询到支付成功时跳转到 /join/paid,避免在 /join 重复显示成功页
|
// 轮询到支付成功时跳转到 /join/paid,携带 order_id 供 complete-order 使用
|
||||||
usePayStatusPoll(() => {
|
usePayStatusPoll((orderId) => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const paidPath = path.replace(/\/join\/?$/, "") + "/join/paid";
|
const paidPath = path.replace(/\/join\/?$/, "") + "/join/paid";
|
||||||
window.location.replace(paidPath);
|
const url = orderId ? `${paidPath}?order_id=${encodeURIComponent(orderId)}` : paidPath;
|
||||||
|
window.location.replace(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -53,18 +53,32 @@ export default function JoinPaidPage() {
|
|||||||
pbSaveAuth(d.token, d.record);
|
pbSaveAuth(d.token, d.record);
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new CustomEvent("auth:updated"));
|
window.dispatchEvent(new CustomEvent("auth:updated"));
|
||||||
setTimeout(() => window.dispatchEvent(new CustomEvent("auth:updated")), 500);
|
window.dispatchEvent(new CustomEvent("vip:updated"));
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("auth:updated"));
|
||||||
|
window.dispatchEvent(new CustomEvent("vip:updated"));
|
||||||
|
}, 400);
|
||||||
if (d.is_new) {
|
if (d.is_new) {
|
||||||
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setNeedPasswordChange(true);
|
setNeedPasswordChange(true);
|
||||||
setShowPasswordModal(true);
|
setShowPasswordModal(true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const needRes = await fetch("/api/meetup/need-password-change", { credentials: "include" });
|
||||||
|
const needData = await needRes.json().catch(() => ({}));
|
||||||
|
if (!cancelled && needData?.need === true) {
|
||||||
|
setNeedPasswordChange(true);
|
||||||
|
setShowPasswordModal(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
const urlOrderId = typeof window !== "undefined"
|
||||||
|
? new URLSearchParams(window.location.search).get("order_id") || ""
|
||||||
|
: "";
|
||||||
const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order="));
|
const cookie = document.cookie.split(";").find((c) => c.trim().startsWith("join_pay_order="));
|
||||||
const rawValue = cookie?.split("=")[1]?.trim();
|
const rawValue = cookie?.split("=")[1]?.trim();
|
||||||
const storageValue = (() => {
|
const storageValue = (() => {
|
||||||
@@ -75,7 +89,7 @@ export default function JoinPaidPage() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
const decoded = rawValue ? decodeURIComponent(rawValue) : storageValue;
|
const decoded = rawValue ? decodeURIComponent(rawValue) : storageValue;
|
||||||
const orderId = decoded.split("|")[0]?.trim() || "";
|
const orderId = urlOrderId || decoded.split("|")[0]?.trim() || "";
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
clearPendingOrder();
|
clearPendingOrder();
|
||||||
window.location.replace(`/${window.location.pathname.split("/")[1] || "zh"}/join`);
|
window.location.replace(`/${window.location.pathname.split("/")[1] || "zh"}/join`);
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ async function verifyOrderPaid(request: NextRequest, orderId: string): Promise<b
|
|||||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
const apiUrl = (
|
const apiUrl = (
|
||||||
getApiUrlFromRequest(hostHeader) ||
|
getApiUrlFromRequest(hostHeader) ||
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
config.apiUrl
|
config.apiUrl
|
||||||
).replace(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
const config = getPaymentConfig();
|
const config = getPaymentConfig();
|
||||||
const hostHeader =
|
const hostHeader =
|
||||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
return (
|
return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
|
||||||
getApiUrlFromRequest(hostHeader) ||
|
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
config.apiUrl
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPayOrderCookie(response: NextResponse, orderId: string) {
|
function setPayOrderCookie(response: NextResponse, orderId: string) {
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
const config = getPaymentConfig();
|
const config = getPaymentConfig();
|
||||||
const hostHeader =
|
const hostHeader =
|
||||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
return (
|
return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
|
||||||
getApiUrlFromRequest(hostHeader) ||
|
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
config.apiUrl
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { Link, useLocale } from "@/i18n/navigation";
|
import { Link, useLocale } from "@/i18n/navigation";
|
||||||
import { useTranslation } from "@/i18n/navigation";
|
import { useTranslation } from "@/i18n/navigation";
|
||||||
import JoinModal from "./JoinModal";
|
import JoinModal from "./JoinModal";
|
||||||
@@ -85,6 +85,9 @@ export default function HeroSection() {
|
|||||||
return () => window.removeEventListener("auth:updated", onAuth);
|
return () => window.removeEventListener("auth:updated", onAuth);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const checkUserCacheRef = useRef<{ email: string; data: { exists: boolean; vip: boolean }; ts: number } | null>(null);
|
||||||
|
const CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
const handleCheckAndProceed = useCallback(
|
const handleCheckAndProceed = useCallback(
|
||||||
async (em: string) => {
|
async (em: string) => {
|
||||||
if (checking) return;
|
if (checking) return;
|
||||||
@@ -96,6 +99,15 @@ export default function HeroSection() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
try {
|
try {
|
||||||
|
const cached = checkUserCacheRef.current;
|
||||||
|
if (cached && cached.email === trimmed && Date.now() - cached.ts < CACHE_TTL_MS) {
|
||||||
|
const { exists, vip } = cached.data;
|
||||||
|
if (exists) {
|
||||||
|
setJoinModalVipOnly(!!vip);
|
||||||
|
setJoinModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const checkRes = await fetch("/api/meetup/check-user", {
|
const checkRes = await fetch("/api/meetup/check-user", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -107,6 +119,7 @@ export default function HeroSection() {
|
|||||||
throw new Error(checkData?.error || checkData?.detail || (locale === "zh" ? "操作失败" : "Operation failed"));
|
throw new Error(checkData?.error || checkData?.detail || (locale === "zh" ? "操作失败" : "Operation failed"));
|
||||||
}
|
}
|
||||||
if (checkData.exists) {
|
if (checkData.exists) {
|
||||||
|
checkUserCacheRef.current = { email: trimmed, data: { exists: true, vip: !!checkData.vip }, ts: Date.now() };
|
||||||
if (checkData.vip) {
|
if (checkData.vip) {
|
||||||
setJoinModalOpen(true);
|
setJoinModalOpen(true);
|
||||||
setJoinModalVipOnly(true);
|
setJoinModalVipOnly(true);
|
||||||
@@ -136,6 +149,9 @@ export default function HeroSection() {
|
|||||||
(locale === "zh" ? "创建用户失败" : "Failed to create user")
|
(locale === "zh" ? "创建用户失败" : "Failed to create user")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (ensureData.is_new) {
|
||||||
|
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
||||||
|
}
|
||||||
user_id = String(ensureData.user_id).trim();
|
user_id = String(ensureData.user_id).trim();
|
||||||
if (ensureData?.token && ensureData?.record) {
|
if (ensureData?.token && ensureData?.record) {
|
||||||
await fetch("/api/auth/sync-session", {
|
await fetch("/api/auth/sync-session", {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly
|
|||||||
if (!res.ok || !data?.ok) {
|
if (!res.ok || !data?.ok) {
|
||||||
throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed"));
|
throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed"));
|
||||||
}
|
}
|
||||||
|
if (data.is_new) {
|
||||||
|
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
||||||
|
}
|
||||||
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
||||||
pbSaveAuth(data.token, data.record);
|
pbSaveAuth(data.token, data.record);
|
||||||
await fetch("/api/auth/sync-session", {
|
await fetch("/api/auth/sync-session", {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation";
|
import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation";
|
||||||
import { getStoredUserEmail, pbLogout } from "@/app/lib/pocketbase";
|
import { getStoredUserEmail, getStoredUser, getStoredToken, pbLogout } from "@/app/lib/pocketbase";
|
||||||
import { getNavLinks } from "@/config";
|
import { getNavLinks } from "@/config";
|
||||||
import AuthModal from "./AuthModal";
|
import AuthModal from "./AuthModal";
|
||||||
import ThemeToggle from "./ThemeToggle";
|
import ThemeToggle from "./ThemeToggle";
|
||||||
@@ -25,8 +25,8 @@ export default function NavbarComponent() {
|
|||||||
|
|
||||||
const fetchAuth = useCallback(async () => {
|
const fetchAuth = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
let res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||||
const data = await res.json();
|
let data = await res.json();
|
||||||
if (data?.user && data?.token) {
|
if (data?.user && data?.token) {
|
||||||
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
||||||
pbSaveAuth(data.token, { id: data.user.id, email: data.user.email });
|
pbSaveAuth(data.token, { id: data.user.id, email: data.user.email });
|
||||||
@@ -34,6 +34,25 @@ export default function NavbarComponent() {
|
|||||||
setUserVip(!!data.user.vip);
|
setUserVip(!!data.user.vip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const token = getStoredToken();
|
||||||
|
const record = getStoredUser();
|
||||||
|
if (token && record?.id && record?.email) {
|
||||||
|
const syncRes = await fetch("/api/auth/sync-session", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ token, record }),
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (syncRes.ok) {
|
||||||
|
res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||||
|
data = await res.json();
|
||||||
|
if (data?.user && data?.token) {
|
||||||
|
setUserEmail(data.user.email);
|
||||||
|
setUserVip(!!data.user.vip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -47,15 +66,21 @@ export default function NavbarComponent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onAuthUpdated = () => fetchAuth();
|
const onAuthUpdated = () => fetchAuth();
|
||||||
|
const onVipUpdated = () => fetchAuth();
|
||||||
window.addEventListener("auth:updated", onAuthUpdated);
|
window.addEventListener("auth:updated", onAuthUpdated);
|
||||||
return () => window.removeEventListener("auth:updated", onAuthUpdated);
|
window.addEventListener("vip:updated", onVipUpdated);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("auth:updated", onAuthUpdated);
|
||||||
|
window.removeEventListener("vip:updated", onVipUpdated);
|
||||||
|
};
|
||||||
}, [fetchAuth]);
|
}, [fetchAuth]);
|
||||||
|
|
||||||
const handleLogout = useCallback(async () => {
|
const handleLogout = useCallback(async () => {
|
||||||
await pbLogout();
|
await pbLogout();
|
||||||
setUserEmail(null);
|
setUserEmail(null);
|
||||||
setUserVip(false);
|
setUserVip(false);
|
||||||
}, []);
|
router.replace("/");
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const isActive = useCallback((href: string) => {
|
const isActive = useCallback((href: string) => {
|
||||||
if (href.startsWith("/#")) return false;
|
if (href.startsWith("/#")) return false;
|
||||||
@@ -124,7 +149,7 @@ export default function NavbarComponent() {
|
|||||||
<span className="hidden sm:block max-w-[120px] truncate text-sm text-gray-600 dark:text-gray-400">
|
<span className="hidden sm:block max-w-[120px] truncate text-sm text-gray-600 dark:text-gray-400">
|
||||||
{userEmail}
|
{userEmail}
|
||||||
</span>
|
</span>
|
||||||
<UserAvatar email={userEmail} isVip={userVip} onLogout={() => { setUserEmail(null); setUserVip(false); }} />
|
<UserAvatar email={userEmail} isVip={userVip} onLogout={handleLogout} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -54,18 +54,27 @@ function parsePendingOrder(raw: string | null): [string | null, boolean] {
|
|||||||
return [orderId, true];
|
return [orderId, true];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePayStatusPoll(onPaid: () => void): boolean {
|
export function usePayStatusPoll(onPaid: (orderId: string) => void): boolean {
|
||||||
const [polling, setPolling] = useState(false);
|
const [polling, setPolling] = useState(false);
|
||||||
const onPaidRef = useRef(onPaid);
|
const onPaidRef = useRef(onPaid);
|
||||||
onPaidRef.current = onPaid;
|
onPaidRef.current = onPaid;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const urlOrderId =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? new URLSearchParams(window.location.search).get("order_id") || ""
|
||||||
|
: "";
|
||||||
const cookieRaw = getCookie(COOKIE_NAME);
|
const cookieRaw = getCookie(COOKIE_NAME);
|
||||||
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
|
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
|
||||||
const storageRaw = getStorage(STORAGE_NAME);
|
const storageRaw = getStorage(STORAGE_NAME);
|
||||||
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
|
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
|
||||||
const orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
|
let orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
|
||||||
const activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
|
let activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
|
||||||
|
if (!orderId && urlOrderId.trim()) {
|
||||||
|
orderId = urlOrderId.trim();
|
||||||
|
activeRaw = `${orderId}|${Date.now()}`;
|
||||||
|
persistPendingOrder(activeRaw);
|
||||||
|
}
|
||||||
|
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
if (cookieRaw || storageRaw) {
|
if (cookieRaw || storageRaw) {
|
||||||
@@ -120,7 +129,7 @@ export function usePayStatusPoll(onPaid: () => void): boolean {
|
|||||||
stop();
|
stop();
|
||||||
clearPendingOrder();
|
clearPendingOrder();
|
||||||
setPolling(false);
|
setPolling(false);
|
||||||
onPaidRef.current();
|
onPaidRef.current(orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -67,11 +67,17 @@ export function pbSaveAuth(token: string, record: AuthUser): void {
|
|||||||
localStorage.setItem(PB_STORAGE_KEYS.user, JSON.stringify(record));
|
localStorage.setItem(PB_STORAGE_KEYS.user, JSON.stringify(record));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登出 - 清除本地存储和根域 Cookie */
|
/** 登出 - 清除本地存储、支付订单、根域 Cookie */
|
||||||
export async function pbLogout(): Promise<void> {
|
export async function pbLogout(): Promise<void> {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
localStorage.removeItem(PB_STORAGE_KEYS.token);
|
localStorage.removeItem(PB_STORAGE_KEYS.token);
|
||||||
localStorage.removeItem(PB_STORAGE_KEYS.user);
|
localStorage.removeItem(PB_STORAGE_KEYS.user);
|
||||||
|
localStorage.removeItem("join_pay_order");
|
||||||
|
try {
|
||||||
|
document.cookie = "join_pay_order=; path=/; max-age=0";
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
|
await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -80,18 +86,24 @@ export async function pbLogout(): Promise<void> {
|
|||||||
window.dispatchEvent(new CustomEvent("auth:updated"));
|
window.dispatchEvent(new CustomEvent("auth:updated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStoredUserEmail(): string | null {
|
export function getStoredUser(): AuthUser | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(PB_STORAGE_KEYS.user);
|
const raw = localStorage.getItem(PB_STORAGE_KEYS.user);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const user = JSON.parse(raw);
|
const user = JSON.parse(raw);
|
||||||
return user?.email ?? null;
|
if (!user?.id || !user?.email) return null;
|
||||||
|
return user;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStoredUserEmail(): string | null {
|
||||||
|
const user = getStoredUser();
|
||||||
|
return user?.email ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getStoredToken(): string | null {
|
export function getStoredToken(): string | null {
|
||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
return localStorage.getItem(PB_STORAGE_KEYS.token);
|
return localStorage.getItem(PB_STORAGE_KEYS.token);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
pbRegister,
|
pbRegister,
|
||||||
pbSaveAuth,
|
pbSaveAuth,
|
||||||
pbLogout,
|
pbLogout,
|
||||||
|
getStoredUser,
|
||||||
getStoredUserEmail,
|
getStoredUserEmail,
|
||||||
getStoredToken,
|
getStoredToken,
|
||||||
} from "./auth";
|
} from "./auth";
|
||||||
|
|||||||
@@ -8,11 +8,20 @@ const isDev = process.env.NODE_ENV === "development";
|
|||||||
export const ROOT_DOMAIN =
|
export const ROOT_DOMAIN =
|
||||||
process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn";
|
process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn";
|
||||||
|
|
||||||
/** 支付 API 默认地址(本地可用 PAYMENT_API_URL 覆盖) */
|
/** 支付 API 默认地址。生产环境固定为 api 域名,避免 .env 中 127.0.0.1 导致支付失败 */
|
||||||
export const DEFAULT_PAYMENT_API_URL = isDev
|
export const DEFAULT_PAYMENT_API_URL = isDev
|
||||||
? "http://127.0.0.1:8007"
|
? "http://127.0.0.1:8007"
|
||||||
: `https://api.${ROOT_DOMAIN}`;
|
: `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_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`;
|
||||||
|
|
||||||
export const DEFAULT_MINIO_HOST = `minioweb.${ROOT_DOMAIN}`;
|
export const DEFAULT_MINIO_HOST = `minioweb.${ROOT_DOMAIN}`;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DEFAULT_PAYMENT_API_URL,
|
DEFAULT_PAYMENT_API_URL,
|
||||||
DEFAULT_POCKETBASE_URL,
|
DEFAULT_POCKETBASE_URL,
|
||||||
DEFAULT_MINIO_HOST,
|
DEFAULT_MINIO_HOST,
|
||||||
|
resolvePaymentApiUrl,
|
||||||
} from "./domain.config";
|
} from "./domain.config";
|
||||||
|
|
||||||
export interface PocketBaseConfig {
|
export interface PocketBaseConfig {
|
||||||
@@ -44,12 +45,12 @@ const PAYJSAPI_PORT = process.env.PAYJSAPI_PORT || "8007";
|
|||||||
/** 站点标识,用于 VIP 按站点隔离 */
|
/** 站点标识,用于 VIP 按站点隔离 */
|
||||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "meetup";
|
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "meetup";
|
||||||
|
|
||||||
/** meetup 加入游牧中国 价格(分),测试用1元 */
|
/** meetup 加入游牧中国 价格(分),88元=8800 */
|
||||||
export const MEETUP_JOIN_AMOUNT = 100;
|
export const MEETUP_JOIN_AMOUNT = 8800;
|
||||||
|
|
||||||
export function getApiUrlFromRequest(hostHeader: string | null): string | null {
|
export function getApiUrlFromRequest(hostHeader: string | null): string | null {
|
||||||
if (!hostHeader) return null;
|
if (!hostHeader) return null;
|
||||||
if (!isDev) return process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL;
|
if (!isDev) return resolvePaymentApiUrl();
|
||||||
const hostname = hostHeader.split(":")[0];
|
const hostname = hostHeader.split(":")[0];
|
||||||
return `http://${hostname}:${PAYJSAPI_PORT}`;
|
return `http://${hostname}:${PAYJSAPI_PORT}`;
|
||||||
}
|
}
|
||||||
@@ -69,9 +70,9 @@ export function getPocketBaseConfig(): PocketBaseConfig {
|
|||||||
export function getPaymentConfig(): PaymentConfig {
|
export function getPaymentConfig(): PaymentConfig {
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
apiUrl: process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL,
|
apiUrl: resolvePaymentApiUrl(),
|
||||||
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
|
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
|
||||||
defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 100,
|
defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 8800,
|
||||||
defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup",
|
defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup",
|
||||||
zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
|
zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user