's'
This commit is contained in:
68
app/api/auth/change-password/route.ts
Normal file
68
app/api/auth/change-password/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 修改密码 - 用于 meetup 支付成功后强制修改默认密码
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig } from "@/config/services.config";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
const COOKIE_NEEDS_PW = "meetup_needs_password_change";
|
||||
|
||||
function getAuthFromCookie(request: NextRequest): { userId: string; token: string } | null {
|
||||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||||
if (payload?.userId && payload?.token) return { userId: payload.userId, token: payload.token };
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const newPassword = String(body?.newPassword || "").trim();
|
||||
const passwordConfirm = String(body?.passwordConfirm || "").trim();
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
return NextResponse.json({ ok: false, error: "密码至少 8 位" }, { status: 400 });
|
||||
}
|
||||
if (newPassword !== passwordConfirm) {
|
||||
return NextResponse.json({ ok: false, error: "两次密码不一致" }, { status: 400 });
|
||||
}
|
||||
|
||||
const auth = getAuthFromCookie(request);
|
||||
if (!auth) {
|
||||
return NextResponse.json({ ok: false, error: "请先登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/collections/users/records/${auth.userId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: newPassword,
|
||||
passwordConfirm: passwordConfirm,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: err?.message || "修改失败" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const r = NextResponse.json({ ok: true });
|
||||
r.cookies.set(COOKIE_NEEDS_PW, "", { path: "/", maxAge: 0 });
|
||||
return r;
|
||||
} catch (e) {
|
||||
console.error("change-password error:", e);
|
||||
return NextResponse.json({ ok: false, error: "操作失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
10
app/api/auth/logout/route.ts
Normal file
10
app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_DOMAIN } from "@/config/domain.config";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
|
||||
export async function POST() {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
res.cookies.set(COOKIE_NAME, "", { domain: AUTH_COOKIE_DOMAIN, path: "/", maxAge: 0 });
|
||||
return res;
|
||||
}
|
||||
60
app/api/auth/me/route.ts
Normal file
60
app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig } from "@/config/services.config";
|
||||
import { AUTH_COOKIE_DOMAIN } from "@/config/domain.config";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (!cookie) {
|
||||
return NextResponse.json({ ok: true, user: null });
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||||
const { token, userId, email } = payload;
|
||||
if (!token || !userId) return NextResponse.json({ ok: true, user: null });
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/users/refresh`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const r = NextResponse.json({ ok: true, user: null });
|
||||
r.cookies.set(COOKIE_NAME, "", { domain: AUTH_COOKIE_DOMAIN, path: "/", maxAge: 0 });
|
||||
return r;
|
||||
}
|
||||
const data = await res.json();
|
||||
const newToken = data.token;
|
||||
const newRecord = data.record;
|
||||
if (newToken && newRecord?.id) {
|
||||
const newPayload = JSON.stringify({
|
||||
token: newToken,
|
||||
userId: newRecord.id,
|
||||
email: newRecord.email ?? email,
|
||||
});
|
||||
const encoded = Buffer.from(newPayload, "utf-8").toString("base64url");
|
||||
const r = NextResponse.json({
|
||||
ok: true,
|
||||
user: { id: newRecord.id, email: newRecord.email ?? email },
|
||||
token: newToken,
|
||||
});
|
||||
r.cookies.set(COOKIE_NAME, encoded, {
|
||||
domain: AUTH_COOKIE_DOMAIN,
|
||||
path: "/",
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
httpOnly: true,
|
||||
});
|
||||
return r;
|
||||
}
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
user: { id: data.record?.id ?? userId, email: data.record?.email ?? email },
|
||||
token: data.token,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ ok: true, user: null });
|
||||
}
|
||||
}
|
||||
32
app/api/auth/sync-session/route.ts
Normal file
32
app/api/auth/sync-session/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_DOMAIN } from "@/config/domain.config";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 365 天,登录态持续有效直至用户主动退出
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const token = body?.token;
|
||||
const record = body?.record;
|
||||
if (!token || !record?.id) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 token 或 record" }, { status: 400 });
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
token,
|
||||
userId: record.id,
|
||||
email: record.email ?? "",
|
||||
});
|
||||
const encoded = Buffer.from(payload, "utf-8").toString("base64url");
|
||||
|
||||
const res = NextResponse.json({ ok: true });
|
||||
res.cookies.set(COOKIE_NAME, encoded, {
|
||||
domain: AUTH_COOKIE_DOMAIN,
|
||||
path: "/",
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
httpOnly: true,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig } from "@/config/services.config";
|
||||
|
||||
function getOrCreateUserId(): string {
|
||||
const COOKIE_NAME = "pb_session";
|
||||
|
||||
function getOrCreateUserId(request: NextRequest): string {
|
||||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (cookie) {
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||||
if (payload?.userId) return payload.userId;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return `user${Date.now()}`;
|
||||
}
|
||||
|
||||
@@ -23,7 +34,7 @@ async function getAdminToken(): Promise<string | null> {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const userId = getOrCreateUserId();
|
||||
const userId = getOrCreateUserId(request);
|
||||
const body = await request.json();
|
||||
const formData = body && typeof body === "object" ? body : {};
|
||||
|
||||
|
||||
110
app/api/meetup/ensure-user/route.ts
Normal file
110
app/api/meetup/ensure-user/route.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 加入游牧中国:确保用户存在
|
||||
* 新用户:用默认密码 123456789 创建,支付成功后需修改
|
||||
* 老用户:验证密码后登录
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig } from "@/config/services.config";
|
||||
|
||||
const DEFAULT_PASSWORD = "123456789";
|
||||
const COOKIE_NEEDS_PW = "meetup_needs_password_change";
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) return null;
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/admins/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.token ?? null;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const email = String(body?.email || "").trim();
|
||||
const password = String(body?.password || "").trim();
|
||||
if (!email) {
|
||||
return NextResponse.json({ ok: false, error: "请输入邮箱" }, { status: 400 });
|
||||
}
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const tryLogin = async (pwd: string) => {
|
||||
const res = await fetch(`${pb.url}/api/collections/users/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password: pwd }),
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
let loginRes = await tryLogin(password || DEFAULT_PASSWORD);
|
||||
if (loginRes.ok) {
|
||||
const data = await loginRes.json();
|
||||
const res = NextResponse.json({
|
||||
ok: true,
|
||||
user_id: data.record?.id,
|
||||
token: data.token,
|
||||
record: data.record,
|
||||
is_new: false,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
const errData = await loginRes.json().catch(() => ({}));
|
||||
const isNotFound = loginRes.status === 400 && (errData?.message?.includes("Invalid") || errData?.message?.includes("identity"));
|
||||
if (!isNotFound && password) {
|
||||
return NextResponse.json({ ok: false, error: "密码错误" }, { status: 400 });
|
||||
}
|
||||
|
||||
const adminToken = await getAdminToken();
|
||||
if (!adminToken) {
|
||||
return NextResponse.json({ ok: false, error: "服务配置错误" }, { status: 500 });
|
||||
}
|
||||
|
||||
const createRes = await fetch(`${pb.url}/api/collections/users/records`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password: DEFAULT_PASSWORD,
|
||||
passwordConfirm: DEFAULT_PASSWORD,
|
||||
}),
|
||||
});
|
||||
if (!createRes.ok) {
|
||||
const createErr = await createRes.json().catch(() => ({}));
|
||||
if (createRes.status === 400 && createErr?.data?.email?.message?.includes("already")) {
|
||||
return NextResponse.json({ ok: false, error: "该邮箱已注册,请输入密码登录" }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ ok: false, error: createErr?.message || "创建账号失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
const newUser = await createRes.json();
|
||||
const finalLogin = await tryLogin(DEFAULT_PASSWORD);
|
||||
if (!finalLogin.ok) {
|
||||
return NextResponse.json({ ok: false, error: "登录失败" }, { status: 500 });
|
||||
}
|
||||
const authData = await finalLogin.json();
|
||||
|
||||
const res = NextResponse.json({
|
||||
ok: true,
|
||||
user_id: authData.record?.id,
|
||||
token: authData.token,
|
||||
record: authData.record,
|
||||
is_new: true,
|
||||
});
|
||||
res.cookies.set(COOKIE_NEEDS_PW, "1", { path: "/", maxAge: 60 * 60 * 24 });
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error("ensure-user error:", e);
|
||||
return NextResponse.json({ ok: false, error: "操作失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
8
app/api/meetup/need-password-change/route.ts
Normal file
8
app/api/meetup/need-password-change/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const COOKIE_NEEDS_PW = "meetup_needs_password_change";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const need = request.cookies.get(COOKIE_NEEDS_PW)?.value === "1";
|
||||
return NextResponse.json({ need });
|
||||
}
|
||||
@@ -14,6 +14,7 @@ async function createPayOrder(
|
||||
|
||||
const payload = {
|
||||
user_id,
|
||||
site_id: "meetup",
|
||||
total_fee: Number(total_fee) || config.defaultAmount,
|
||||
type: type || config.defaultType,
|
||||
channel,
|
||||
@@ -23,7 +24,7 @@ async function createPayOrder(
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const res = await fetch(`${config.apiUrl}/payh5`, {
|
||||
const res = await fetch(`${config.apiUrl}/cnomadcna/payh5`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -56,7 +57,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const user_id = String(body?.user_id || "").trim();
|
||||
const return_url = String(body?.return_url || "").trim() || `${SITE_URL}/zh/join/paid`;
|
||||
const total_fee = Number(body?.total_fee) || 80;
|
||||
const total_fee = Number(body?.total_fee) || 100;
|
||||
const type = String(body?.type || "meetup");
|
||||
const channel = String(body?.channel || "alipay");
|
||||
const device = String(body?.device || "pc");
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 25000);
|
||||
const res = await fetch(
|
||||
`${config.apiUrl}/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
|
||||
`${config.apiUrl}/cnomadcna/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
|
||||
{ cache: "no-store", signal: controller.signal }
|
||||
).finally(() => clearTimeout(timeout));
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
55
app/api/vip/check/route.ts
Normal file
55
app/api/vip/check/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPocketBaseConfig, SITE_ID } from "@/config/services.config";
|
||||
|
||||
const COOKIE_NAME = "pb_session";
|
||||
|
||||
async function getAdminToken(): Promise<string | null> {
|
||||
const email = process.env.POCKETBASE_EMAIL;
|
||||
const password = process.env.POCKETBASE_PASSWORD;
|
||||
if (!email || !password) return null;
|
||||
const pb = getPocketBaseConfig();
|
||||
const res = await fetch(`${pb.url}/api/admins/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: email, password }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.token ?? null;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||||
if (!cookie) {
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||||
const userId = payload?.userId;
|
||||
if (!userId) return NextResponse.json({ ok: true, vip: false });
|
||||
|
||||
const token = await getAdminToken();
|
||||
if (!token) return NextResponse.json({ ok: true, vip: false });
|
||||
|
||||
const pb = getPocketBaseConfig();
|
||||
const filter = `user_id = "${userId}" && site_id = "${SITE_ID}"`;
|
||||
const res = await fetch(
|
||||
`${pb.url}/api/collections/site_vip/records?filter=${encodeURIComponent(filter)}&perPage=1`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
if (!res.ok) return NextResponse.json({ ok: true, vip: false });
|
||||
const data = await res.json();
|
||||
const items = data?.items ?? [];
|
||||
const record = items[0];
|
||||
const now = new Date().toISOString();
|
||||
const isExpired = record?.expires_at && record.expires_at < now;
|
||||
const vip = !!record && !isExpired;
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
vip,
|
||||
expires_at: record?.expires_at ?? null,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ ok: true, vip: false });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user