feat(legal): terms and privacy pages, login consent recorded per version, re-consent gate (draft text, placeholders)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
788a84a4c0
commit
8f58eccecf
@@ -0,0 +1,63 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { CURRENT_LEGAL_VERSION, LEGAL_CONSENT_DOCUMENTS, LEGAL_RECONSENT_BLOCKING, consentOutdated, isCurrentConsentRequest, latestAccepted } from "@/lib/legal-consent";
|
||||
import { consentIpHash } from "@/lib/legal-consent-server";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
type ConsentRow = { document: string; version: string; accepted_at: string };
|
||||
|
||||
/**
|
||||
* GET: which versions this user accepted, and whether a (blocking) re-consent
|
||||
* is required now. POST: record acceptance of the current version of both
|
||||
* documents. Rows are append-only (see 20260930010000_user_consents.sql).
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
const { data, error } = await supabase.from("user_consents").select("document,version,accepted_at").eq("user_id", user.id);
|
||||
if (error) return NextResponse.json({ error: "暂时读不到同意记录" }, { status: 503 });
|
||||
const accepted = latestAccepted((data ?? []) as ConsentRow[]);
|
||||
return NextResponse.json({
|
||||
current: CURRENT_LEGAL_VERSION,
|
||||
accepted,
|
||||
required: LEGAL_RECONSENT_BLOCKING && consentOutdated(accepted),
|
||||
});
|
||||
} catch (caught) {
|
||||
if (isSupabaseConfigurationError(caught)) return NextResponse.json({ error: "服务未配置" }, { status: 503 });
|
||||
throw caught;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
if (!isCurrentConsentRequest(await request.json().catch(() => null))) {
|
||||
return NextResponse.json({ error: "协议版本已更新,请刷新页面后重新确认" }, { status: 409 });
|
||||
}
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
const { data: existing, error: readError } = await supabase
|
||||
.from("user_consents").select("document,version,accepted_at")
|
||||
.eq("user_id", user.id).eq("version", CURRENT_LEGAL_VERSION);
|
||||
if (readError) return NextResponse.json({ error: "暂时无法记录" }, { status: 503 });
|
||||
const have = new Set(((existing ?? []) as ConsentRow[]).map(row => row.document));
|
||||
const missing = LEGAL_CONSENT_DOCUMENTS.filter(document => !have.has(document));
|
||||
if (missing.length) {
|
||||
const ipHash = consentIpHash(request.headers);
|
||||
const userAgent = request.headers.get("user-agent")?.slice(0, 160) ?? null;
|
||||
const { error } = await supabase.from("user_consents").insert(missing.map(document => ({
|
||||
user_id: user.id, document, version: CURRENT_LEGAL_VERSION, ip_hash: ipHash, user_agent: userAgent,
|
||||
})));
|
||||
if (error) return NextResponse.json({ error: "暂时无法记录" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ recorded: missing, version: CURRENT_LEGAL_VERSION });
|
||||
} catch (caught) {
|
||||
if (isSupabaseConfigurationError(caught)) return NextResponse.json({ error: "服务未配置" }, { status: 503 });
|
||||
throw caught;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user