0.3 增加登录模块
This commit is contained in:
@@ -20,7 +20,7 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
const openTableId = typeof openTableIdRaw === "string" ? openTableIdRaw : null;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
const doc = await client.query(api.documents.getMeta, { userId: auth.userId, id });
|
||||
if (!doc) {
|
||||
|
||||
@@ -66,7 +66,7 @@ function makeId(): string {
|
||||
|
||||
export default async function AppLayout({ children }: { children: ReactNode }) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const ensured = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client";
|
||||
|
||||
import { useConvexAuth } from "convex/react";
|
||||
import { useAuthActions } from "@convex-dev/auth/react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
type AuthStep = "signIn" | "signUp";
|
||||
|
||||
// 测试账号凭据常量
|
||||
const TEST_CREDENTIALS = {
|
||||
email: "test@example.com",
|
||||
password: "Test123456",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Convex Auth 登录/注册页面
|
||||
*
|
||||
* 支持功能:
|
||||
* - 邮箱密码登录
|
||||
* - 邮箱密码注册
|
||||
*/
|
||||
export default function AuthPage() {
|
||||
const { isLoading, isAuthenticated } = useConvexAuth();
|
||||
const { signIn } = useAuthActions();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && isAuthenticated) {
|
||||
router.replace("/");
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
const [flow, setFlow] = useState<AuthStep>("signIn");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [message, setMessage] = useState<{
|
||||
type: "success" | "error" | "info";
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
// 执行登录的核心逻辑
|
||||
const performSignIn = useCallback(async (email: string, password: string, flow: AuthStep) => {
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("email", email);
|
||||
formData.append("password", password);
|
||||
if (flow === "signUp") {
|
||||
formData.append("name", name);
|
||||
}
|
||||
formData.append("flow", flow);
|
||||
|
||||
const result = await signIn("password", formData);
|
||||
|
||||
if (result.signingIn) {
|
||||
setMessage({ type: "success", text: flow === "signIn" ? "登录成功!" : "注册成功!" });
|
||||
setTimeout(() => router.push("/"), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 说明:部分认证方式会返回 redirect(例如 OAuth);密码模式一般不会走到这里。
|
||||
if (result.redirect) {
|
||||
setMessage({ type: "info", text: "正在跳转..." });
|
||||
router.push(result.redirect.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage({ type: "error", text: "操作失败,请重试" });
|
||||
} catch (error: any) {
|
||||
setMessage({ type: "error", text: error.message || "操作失败,请重试" });
|
||||
}
|
||||
}, [name, signIn, router]);
|
||||
|
||||
const handleSubmit = useCallback(async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
await performSignIn(email, password, flow);
|
||||
}, [email, password, flow, performSignIn]);
|
||||
|
||||
// 检查是否启用了 Convex
|
||||
const isConvex = isConvexEnabled();
|
||||
if (!isConvex) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-red-600 mb-4">认证功能不可用</h1>
|
||||
<p className="text-gray-600">请启用 Convex 后端后再试</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">正在跳转...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
{flow === "signIn" ? "登录账户" : "创建账户"}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
使用 Convex Auth
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`rounded-md p-4 ${
|
||||
message.type === "success" ? "bg-green-50 text-green-800" :
|
||||
message.type === "error" ? "bg-red-50 text-red-800" :
|
||||
"bg-blue-50 text-blue-800"
|
||||
}`}>
|
||||
<p className="text-sm">{message.text}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">邮箱地址</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="邮箱地址"
|
||||
/>
|
||||
</div>
|
||||
{flow === "signUp" && (
|
||||
<div>
|
||||
<label htmlFor="name" className="sr-only">姓名</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="姓名(可选)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete={flow === "signIn" ? "current-password" : "new-password"}
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="密码(至少 8 位)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
{flow === "signIn" ? "登录" : "注册"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{flow === "signIn" && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEmail(TEST_CREDENTIALS.email);
|
||||
setPassword(TEST_CREDENTIALS.password);
|
||||
// 直接调用登录逻辑
|
||||
performSignIn(TEST_CREDENTIALS.email, TEST_CREDENTIALS.password, "signIn");
|
||||
}}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||
>
|
||||
测试账号快速登录
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFlow(flow === "signIn" ? "signUp" : "signIn");
|
||||
setMessage(null);
|
||||
}}
|
||||
className="text-blue-600 hover:text-blue-500 text-sm"
|
||||
>
|
||||
{flow === "signIn" ? "还没有账户?立即注册" : "已有账户?去登录"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,13 +28,11 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "缺少 requestId/callId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const userId = (() => {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
return auth.userId;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
let userId: string | null = null;
|
||||
if (isConvexEnabled()) {
|
||||
const auth = await requireAuthContext();
|
||||
userId = auth.userId;
|
||||
}
|
||||
|
||||
const resolvedUserId = async () => {
|
||||
if (userId) return userId;
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function POST(request: Request) {
|
||||
const convexOn = isConvexEnabled();
|
||||
const { userId, supabase, convexClient } = await (async () => {
|
||||
if (convexOn) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
return { userId: auth.userId, supabase: null as any, convexClient: client };
|
||||
}
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
interface SignInRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name?: string;
|
||||
flow: "signIn" | "signUp";
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/signin
|
||||
*
|
||||
* 处理登录/注册请求
|
||||
* - 在 Convex 模式下调用 Convex mutation
|
||||
* - 在 Supabase 模式下调用 Supabase Auth
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
try {
|
||||
const body = await request.json() as SignInRequest;
|
||||
const { email, password, name, flow } = body;
|
||||
|
||||
// TODO: 调用 Convex Auth mutation 处理登录/注册
|
||||
// 当前暂时返回未实现错误
|
||||
return NextResponse.json(
|
||||
{ error: "Convex Auth API 路由暂未实现,请稍后实现" },
|
||||
{ status: 501 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Convex Auth error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "服务器错误" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式(保留兼容)
|
||||
try {
|
||||
const { createSupabaseRouteClient } = await import("@/lib/supabase/server");
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
|
||||
const body = await request.json() as SignInRequest;
|
||||
const { email, password, name, flow } = body;
|
||||
|
||||
if (flow === "signUp") {
|
||||
// 注册
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
name: name ?? "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 注册成功后自动登录
|
||||
const { error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (signInError) {
|
||||
return NextResponse.json(
|
||||
{ error: signInError.message },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "注册并登录成功" },
|
||||
{ status: 200 }
|
||||
);
|
||||
} else {
|
||||
// 登录
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "登录成功" },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Supabase Auth error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "服务器错误" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const source = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
|
||||
@@ -113,4 +113,3 @@ export async function POST(request: Request) {
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
const doc = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
|
||||
if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 });
|
||||
@@ -46,4 +46,3 @@ export async function GET(request: Request) {
|
||||
|
||||
return NextResponse.json({ ok: true, block: hit.block });
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const source = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
|
||||
@@ -97,4 +97,3 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
const doc = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
|
||||
if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 });
|
||||
@@ -62,4 +62,3 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireAuthContext } from "@/lib/auth/authContext";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Payload =
|
||||
| { type: "document"; documentId: string }
|
||||
| { type: "mindmap"; docId: string; mindmapId: string }
|
||||
| { type: "media"; assetId: string };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const body = (await request.json().catch(() => null)) as Payload | null;
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: "缺少请求体" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (body.type === "document") {
|
||||
const documentId = String(body.documentId ?? "").trim();
|
||||
if (!documentId) return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
const result = await client.mutation(api.jobs.enqueueRagIndexDocument, { userId: auth.userId, documentId });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
if (body.type === "mindmap") {
|
||||
const docId = String(body.docId ?? "").trim();
|
||||
const mindmapId = String(body.mindmapId ?? "").trim();
|
||||
if (!docId) return NextResponse.json({ error: "缺少 docId" }, { status: 400 });
|
||||
if (!mindmapId) return NextResponse.json({ error: "缺少 mindmapId" }, { status: 400 });
|
||||
const result = await client.mutation(api.jobs.enqueueRagIndexMindmap, { userId: auth.userId, docId, mindmapId });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
if (body.type === "media") {
|
||||
const assetId = String(body.assetId ?? "").trim();
|
||||
if (!assetId) return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
|
||||
const result = await client.mutation(api.jobs.enqueueRagIndexMediaAsset, { userId: auth.userId, assetId });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "不支持的 type" }, { status: 400 });
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { api } from "@/lib/convex/api";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const { ms }: { ms?: number } = await request.json().catch(() => ({}));
|
||||
@@ -23,7 +23,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const url = new URL(request.url);
|
||||
@@ -39,4 +39,3 @@ export async function GET(request: Request) {
|
||||
|
||||
return NextResponse.json(job);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
return NextResponse.json({ ok: true, auth }, { status: 200 });
|
||||
} catch (err) {
|
||||
const status = err instanceof HttpError ? err.status : 500;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const url = new URL(request.url);
|
||||
const documentId = url.searchParams.get("documentId") ?? "";
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ function replaceAssetRefsInContent(content: Json | null, assetMap: Map<string, {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const payload = (await request.json()) as CopyTreePayload;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const { parentId, title, blocks }: CreateChildPayload = await request.json();
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
async function handleCreateRequestConvex(request: Request) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const { parentId }: { parentId?: string | null } = await request.json();
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId } = await request.json();
|
||||
if (!documentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
|
||||
@@ -48,7 +48,7 @@ async function copyMindmapIfExists(sourceId: string, targetId: string) {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const { documentId }: DuplicatePayload = await request.json();
|
||||
|
||||
@@ -14,7 +14,7 @@ interface EmbedPayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
|
||||
const { sourceId, targetId }: EmbedPayload = await request.json();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ interface EmptyTrashPayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { workspaceId }: EmptyTrashPayload = await request.json();
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
|
||||
|
||||
@@ -13,7 +13,7 @@ interface MovePayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId, parentId = null, position }: MovePayload = await request.json();
|
||||
const sortOrder = Number.isFinite(position) ? Math.floor(position) : 0;
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
@@ -24,7 +24,7 @@ const COLUMN_MAP: Record<keyof PageOptionsState, keyof Database["public"]["Table
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId, options }: OptionsPayload = await request.json();
|
||||
if (!documentId || !options) {
|
||||
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId } = await request.json();
|
||||
if (!documentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId } = await request.json();
|
||||
if (!documentId) {
|
||||
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SavePayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId, content }: SavePayload = await request.json();
|
||||
const client = getConvexHttpClient();
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
|
||||
@@ -13,7 +13,7 @@ interface StatsPayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId, stats }: StatsPayload = await request.json();
|
||||
if (!documentId || !stats) {
|
||||
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
|
||||
|
||||
@@ -12,7 +12,7 @@ interface RenamePayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const { documentId, title }: RenamePayload = await request.json();
|
||||
const client = getConvexHttpClient();
|
||||
await client.mutation(api.documents.updateTitle, {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
@@ -11,6 +14,28 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ code: 400, msg: "gridKey 参数缺失" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
try {
|
||||
const client = getConvexHttpClient();
|
||||
const result = await client.query(api.tables.getByGridKey, { gridKey });
|
||||
|
||||
if (!result) {
|
||||
return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
code: 200,
|
||||
msg: "ok",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ code: 500, msg: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const { data, error } = await supabase
|
||||
.from("document_tables")
|
||||
|
||||
@@ -4,6 +4,9 @@ import { NextResponse } from "next/server";
|
||||
import { createDefaultTableSnapshot } from "@/lib/online-table";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import type { DocumentTableSnapshot, TableSchema } from "@/types/online-table";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
const fallbackSchema: TableSchema = {
|
||||
columns: [],
|
||||
@@ -19,6 +22,38 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ code: 400, msg: "gridKey 参数缺失" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
try {
|
||||
const client = getConvexHttpClient();
|
||||
const tableData = await client.query(api.tables.getByGridKeyFull, { gridKey });
|
||||
|
||||
if (!tableData) {
|
||||
return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 });
|
||||
}
|
||||
|
||||
const snapshot = (tableData.snapshot as DocumentTableSnapshot | null) ?? null;
|
||||
const schema = (tableData.schema as TableSchema | null) ?? fallbackSchema;
|
||||
|
||||
let payload: unknown[] = [];
|
||||
if (snapshot?.luckysheet && Array.isArray(snapshot.luckysheet)) {
|
||||
payload = snapshot.luckysheet;
|
||||
} else {
|
||||
const defaultSnapshot = createDefaultTableSnapshot(schema);
|
||||
payload = defaultSnapshot.luckysheet ?? [];
|
||||
}
|
||||
|
||||
const body = JSON.stringify(payload);
|
||||
return new NextResponse(body, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ code: 500, msg: "服务器错误" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const { data, error } = await supabase
|
||||
.from("document_tables")
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
@@ -85,7 +85,7 @@ export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function GET(request: Request) {
|
||||
// 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。
|
||||
// 由于外链本身已经是可访问的 URL,这里只做最小透传。
|
||||
try {
|
||||
requireAuthContext();
|
||||
await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
auth = await requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
|
||||
@@ -33,7 +33,7 @@ async function purgeTrashFolder(folder: string): Promise<number> {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({}));
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function POST(
|
||||
const { docId, mindmapId } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
|
||||
const payload = (await request.json().catch(() => null)) as RequestPayload | null;
|
||||
const ops = Array.isArray(payload?.ops) ? payload!.ops : [];
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function GET(
|
||||
const { docId, mindmapId } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const res = await client.query(api.mindmaps.get, {
|
||||
userId: auth.userId,
|
||||
docId,
|
||||
@@ -171,7 +171,7 @@ export async function POST(
|
||||
const { docId, mindmapId } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { data, createOnly } = (await request.json().catch(() => ({ data: null }))) as {
|
||||
data?: unknown;
|
||||
createOnly?: boolean;
|
||||
@@ -241,7 +241,7 @@ export async function DELETE(
|
||||
const { docId, mindmapId } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
try {
|
||||
const result = await client.mutation(api.mindmaps.softDelete, {
|
||||
userId: auth.userId,
|
||||
@@ -316,7 +316,7 @@ export async function PATCH(
|
||||
const { docId, mindmapId } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { action } = (await request.json().catch(() => ({}))) as { action?: string };
|
||||
if (action !== "restore" && action !== "purge") {
|
||||
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function GET(
|
||||
const { docId: id } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const mindmapId = `legacy-${id}`;
|
||||
const res = await client.query(api.mindmaps.get, {
|
||||
userId: auth.userId,
|
||||
@@ -106,7 +106,7 @@ export async function POST(
|
||||
const { docId: id } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const mindmapId = `legacy-${id}`;
|
||||
const { data } = await request.json();
|
||||
const result = await client.mutation(api.mindmaps.put, {
|
||||
@@ -161,7 +161,7 @@ export async function DELETE(
|
||||
const { docId: id } = await params;
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const mindmapId = `legacy-${id}`;
|
||||
const result = await client.mutation(api.mindmaps.softDelete, {
|
||||
userId: auth.userId,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api } from "@/lib/convex/api";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = searchParams.get("workspaceId");
|
||||
const pageId = searchParams.get("pageId");
|
||||
|
||||
@@ -20,7 +20,7 @@ const isValidDisplayMode = (mode: string): mode is DisplayMode => mode === "inli
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const body = (await request.json()) as RecordReferencePayload;
|
||||
if (!body?.workspaceId || !body?.sourcePageId || !body?.targetPageId) {
|
||||
return NextResponse.json({ error: "缺少必要参数" }, { status: 400 });
|
||||
|
||||
@@ -148,7 +148,7 @@ const fetchOcrMatches = async (
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const payload = (await request.json()) as DocumentSearchRequest;
|
||||
const workspaceId = payload.workspaceId;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ interface RecentPayload {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const { workspaceId, documentId }: RecentPayload = await request.json();
|
||||
|
||||
if (!workspaceId || !documentId) {
|
||||
|
||||
@@ -72,7 +72,7 @@ function extractMindmapImageAssetIdsFromData(input: unknown): string[] {
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import type { DocumentTableSnapshot, TableRowData } from "@/types/online-table";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{
|
||||
@@ -35,19 +38,38 @@ const getAuthUser = async () => {
|
||||
};
|
||||
|
||||
export async function GET(request: Request, context: RouteContext) {
|
||||
const tableId = await extractTableId(context);
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
try {
|
||||
const table = await client.query(api.tables.get, {
|
||||
userId: auth.userId,
|
||||
tableId,
|
||||
});
|
||||
|
||||
if (!table) {
|
||||
return NextResponse.json({ error: "Table not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(table, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const { supabase, user } = await getAuthUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// 强制 await params,以解决 Next.js 16/Turbopack 错误。
|
||||
const tableId = await extractTableId(context);
|
||||
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 假设 RLS 确保了用户只能访问其有权限的表格
|
||||
const { data: tableData, error } = await supabase
|
||||
@@ -74,17 +96,47 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request, context: RouteContext) {
|
||||
const tableId = await extractTableId(context);
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
try {
|
||||
const body = await request.json() as UpdateTableRequest;
|
||||
|
||||
const result = await client.mutation(api.tables.update, {
|
||||
userId: auth.userId,
|
||||
tableId,
|
||||
title: body.title,
|
||||
schema: body.schema,
|
||||
view_preferences: body.viewPreferences,
|
||||
snapshot: body.snapshot,
|
||||
rows: body.rows,
|
||||
});
|
||||
|
||||
// 重新获取更新后的表格数据
|
||||
const updated = await client.query(api.tables.get, {
|
||||
userId: auth.userId,
|
||||
tableId,
|
||||
});
|
||||
|
||||
return NextResponse.json(updated, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const { supabase, user } = await getAuthUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const tableId = await extractTableId(context);
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
let body: UpdateTableRequest | null = null;
|
||||
try {
|
||||
body = await request.json() as UpdateTableRequest;
|
||||
@@ -191,17 +243,34 @@ export async function PATCH(request: Request, context: RouteContext) {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, context: RouteContext) {
|
||||
const tableId = await extractTableId(context);
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
try {
|
||||
await client.mutation(api.tables.purge, {
|
||||
userId: auth.userId,
|
||||
tableId,
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const { supabase, user } = await getAuthUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const tableId = await extractTableId(context);
|
||||
if (!tableId) {
|
||||
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { error: deleteRowsError } = await supabase
|
||||
.from("document_table_rows")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { DocumentTableSnapshot, TableSchema } from "@/types/online-table";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
// 定义请求体类型
|
||||
interface CreateTableRequestBody {
|
||||
@@ -11,6 +14,46 @@ interface CreateTableRequestBody {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Convex 模式
|
||||
if (isConvexEnabled()) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
|
||||
try {
|
||||
const { documentId, title, schema, snapshot } = await request.json() as CreateTableRequestBody;
|
||||
|
||||
if (!documentId || !title || !schema) {
|
||||
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取 document 所在的 workspace_id
|
||||
const document = await client.query(api.documents.getMeta, {
|
||||
userId: auth.userId,
|
||||
id: documentId,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return NextResponse.json({ error: "Document not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// 创建表格
|
||||
const result = await client.mutation(api.tables.create, {
|
||||
userId: auth.userId,
|
||||
workspaceId: document.workspace_id,
|
||||
documentId,
|
||||
title,
|
||||
schema,
|
||||
snapshot,
|
||||
});
|
||||
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Convex API error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Supabase 模式
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
@@ -9,7 +9,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const payload = await request.json().catch(() => ({}));
|
||||
const workspaceId = payload.workspaceId as string | undefined;
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { SupabaseProvider } from "@/components/providers/supabase-provider";
|
||||
import { QueryProvider } from "@/components/providers/query-provider";
|
||||
import { ConvexClientProvider } from "@/components/providers/convex-provider";
|
||||
import { createSupabaseServerClient } from "@/lib/supabase/server";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -51,7 +53,11 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
{useConvex ? (
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
<ConvexAuthNextjsServerProvider>
|
||||
<ConvexClientProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</ConvexClientProvider>
|
||||
</ConvexAuthNextjsServerProvider>
|
||||
) : (
|
||||
<SupabaseProvider session={session}>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
|
||||
@@ -14,7 +14,7 @@ function makeId(): string {
|
||||
|
||||
export default async function Home() {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const { activeWorkspaceId } = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState } from "react";
|
||||
import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
|
||||
interface ConvexClientProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convex 客户端 Provider(包含 Auth)
|
||||
*/
|
||||
export function ConvexClientProvider({ children }: ConvexClientProviderProps) {
|
||||
const [convex] = useState(() => {
|
||||
// 动态获取 Convex URL,默认优先使用环境变量。
|
||||
// 说明:浏览器侧优先用当前 hostname(端口固定 3210),避免通过非 localhost/127 访问前端时出现跨域/白名单不匹配。
|
||||
const getConvexUrl = () => {
|
||||
// 默认使用环境变量
|
||||
const envUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
|
||||
if (typeof window === "undefined") {
|
||||
return envUrl || "http://127.0.0.1:3210";
|
||||
}
|
||||
// 在浏览器中,使用当前主机名,但端口改为 3210
|
||||
const hostname = window.location.hostname;
|
||||
return `http://${hostname}:3210`;
|
||||
};
|
||||
|
||||
const convexUrl = getConvexUrl();
|
||||
if (!convexUrl) {
|
||||
throw new Error("NEXT_PUBLIC_CONVEX_URL is not defined");
|
||||
}
|
||||
return new ConvexReactClient(convexUrl);
|
||||
});
|
||||
|
||||
return (
|
||||
<ConvexAuthNextjsProvider client={convex}>
|
||||
{children}
|
||||
</ConvexAuthNextjsProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { AuthContext } from "@/lib/auth/types";
|
||||
import { getDevUser, isDevAuthEnabled } from "@/lib/auth/devUser";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server";
|
||||
import { fetchQuery } from "convex/nextjs";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
export class HttpError extends Error {
|
||||
status: number;
|
||||
@@ -11,17 +15,79 @@ export class HttpError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthContext(): AuthContext {
|
||||
if (isDevAuthEnabled()) return getDevUser();
|
||||
// 说明:后续接入真实鉴权时,在这里替换为 Supabase/Convex Auth 的校验逻辑。
|
||||
/**
|
||||
* 获取认证上下文
|
||||
*
|
||||
* 优先级:
|
||||
* 1. Convex Auth(如果启用)- 服务端:从 Convex Auth cookies 获取并查询当前用户;客户端:从 hooks 获取
|
||||
* 2. 开发用户(如果启用)
|
||||
* 3. 抛出错误(未配置)
|
||||
*/
|
||||
export async function getAuthContext(): Promise<AuthContext> {
|
||||
// 1. 检查是否启用 Convex Auth
|
||||
if (isConvexEnabled()) {
|
||||
// 说明:开发模式下允许使用固定用户快速跑通链路(例如迁移测试)。
|
||||
if (isDevAuthEnabled()) {
|
||||
return getDevUser();
|
||||
}
|
||||
|
||||
const token = await convexAuthNextjsToken();
|
||||
if (!token) {
|
||||
throw new Error("未登录,请先登录");
|
||||
}
|
||||
|
||||
// 说明:从 Convex Auth token 获取当前用户(users 表由 authTables 提供)。
|
||||
// 这里不强依赖具体字段结构,避免升级 authTables 后类型不兼容。
|
||||
const user = (await fetchQuery(api.users.currentUser, {}, { token })) as unknown;
|
||||
if (!user || typeof user !== "object") {
|
||||
throw new Error("未登录,请先登录");
|
||||
}
|
||||
|
||||
const record = user as Record<string, unknown>;
|
||||
const id = record["_id"];
|
||||
const email = record["email"];
|
||||
const name = record["name"];
|
||||
|
||||
if (typeof id !== "string" || !id) {
|
||||
throw new Error("未登录,请先登录");
|
||||
}
|
||||
|
||||
return {
|
||||
userId: id,
|
||||
email: typeof email === "string" ? email : undefined,
|
||||
name: typeof name === "string" ? name : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查是否启用开发用户模式
|
||||
if (isDevAuthEnabled()) {
|
||||
return getDevUser();
|
||||
}
|
||||
|
||||
// 3. 未配置认证
|
||||
throw new Error("Auth is not configured");
|
||||
}
|
||||
|
||||
export function requireAuthContext(): AuthContext {
|
||||
/**
|
||||
* 要求已认证的上下文
|
||||
*
|
||||
* 如果未认证,抛出 401 错误
|
||||
*/
|
||||
export async function requireAuthContext(): Promise<AuthContext> {
|
||||
try {
|
||||
return getAuthContext();
|
||||
return await getAuthContext();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unauthorized";
|
||||
throw new HttpError(401, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户 ID
|
||||
*
|
||||
* 用于服务端组件和 API 路由
|
||||
*/
|
||||
export async function getCurrentUserId(): Promise<string> {
|
||||
const auth = await requireAuthContext();
|
||||
return auth.userId;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function isConvexEnabled(): boolean {
|
||||
return process.env.USE_CONVEX === "1";
|
||||
return process.env.NEXT_PUBLIC_USE_CONVEX === "1";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ import type { AuthContext } from "@/lib/auth/types";
|
||||
import { requireAuthContext } from "@/lib/auth/authContext";
|
||||
import { getConvexHttpClient } from "@/lib/convex/server";
|
||||
|
||||
export function getAuthedConvexClient(): { auth: AuthContext; client: ConvexHttpClient } {
|
||||
const auth = requireAuthContext();
|
||||
export async function getAuthedConvexClient(): Promise<{ auth: AuthContext; client: ConvexHttpClient }> {
|
||||
const auth = await requireAuthContext();
|
||||
const client = getConvexHttpClient();
|
||||
return { auth, client };
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@ export function getConvexHttpClient(): ConvexHttpClient {
|
||||
const client = new ConvexHttpClient(url);
|
||||
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY;
|
||||
if (adminKey) {
|
||||
client.setAdminAuth(adminKey);
|
||||
// 说明:ConvexHttpClient 的类型声明可能未暴露 setAdminAuth(但运行期存在)。
|
||||
// 这里用最小侵入方式兼容自托管管理密钥。
|
||||
(client as any).setAdminAuth(adminKey);
|
||||
}
|
||||
|
||||
cached = client;
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { convexAuthNextjsMiddleware, createRouteMatcher, nextjsMiddlewareRedirect } from "@convex-dev/auth/nextjs/server";
|
||||
|
||||
// 说明:
|
||||
// - 这里启用 Convex Auth 的 Next.js 中间件,负责:
|
||||
// 1) 代理 /api/auth 到 Convex(用于 signIn / signOut 等动作)
|
||||
// 2) 刷新/同步 auth cookies
|
||||
// 3) 对需要登录的页面做统一拦截
|
||||
// - 默认将未登录用户重定向到 /auth(Convex Auth 登录页)。
|
||||
|
||||
const isPublicRoute = createRouteMatcher([
|
||||
"/auth",
|
||||
"/login",
|
||||
"/api/auth(.*)",
|
||||
"/api/health(.*)",
|
||||
"/_next(.*)",
|
||||
"/favicon.ico",
|
||||
]);
|
||||
|
||||
export default convexAuthNextjsMiddleware(async (request, ctx) => {
|
||||
// 公共路由不做拦截(但 middleware 仍会处理 token 刷新/代理等)。
|
||||
if (isPublicRoute(request)) return;
|
||||
|
||||
// 只在启用 Convex 模式时做鉴权拦截;否则走 Supabase 逻辑(页面内部自行处理)。
|
||||
// 注意:middleware 运行在 Edge/Node 环境中,读取到的是运行期环境变量。
|
||||
if (process.env.NEXT_PUBLIC_USE_CONVEX !== "1") return;
|
||||
|
||||
const authed = await ctx.convexAuth.isAuthenticated();
|
||||
if (!authed) {
|
||||
return nextjsMiddlewareRedirect(request, "/auth");
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
// 说明:排除静态资源,避免无意义的中间件开销。
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user