fix: make chat session deletion server controlled (#19)
This commit is contained in:
@@ -47,6 +47,7 @@ jobs:
|
||||
frontend/supabase/migrations/20260718102000_recover_missing_profile_rows.sql \
|
||||
frontend/supabase/migrations/20260718103000_profile_birth_time_declaration_grants.sql \
|
||||
frontend/supabase/migrations/20260718104000_chart_profiles_upsert_id_grant.sql \
|
||||
frontend/supabase/migrations/20260721100000_chat_sessions_delete_grant.sql \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/tmp/profile-migrations/"
|
||||
|
||||
- name: Apply profile migrations using VPS database URL
|
||||
@@ -76,7 +77,8 @@ jobs:
|
||||
tmp/profile-migrations/20260718100000_repair_missing_chart_profiles.sql \
|
||||
tmp/profile-migrations/20260718102000_recover_missing_profile_rows.sql \
|
||||
tmp/profile-migrations/20260718103000_profile_birth_time_declaration_grants.sql \
|
||||
tmp/profile-migrations/20260718104000_chart_profiles_upsert_id_grant.sql
|
||||
tmp/profile-migrations/20260718104000_chart_profiles_upsert_id_grant.sql \
|
||||
tmp/profile-migrations/20260721100000_chat_sessions_delete_grant.sql
|
||||
do
|
||||
echo "applying $(basename "$SQL_FILE")"
|
||||
cat "$SQL_FILE" | docker run --rm -i postgres:16-alpine \
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { isSupabaseConfigurationError } from "@/lib/supabase/config";
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function DELETE(_request: Request, context: RouteContext) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 });
|
||||
|
||||
const { count, error } = await supabase
|
||||
.from("chat_sessions")
|
||||
.delete({ count: "exact" })
|
||||
.eq("id", id)
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
if (count !== 1) return NextResponse.json({ error: "聊天记录不存在或无权删除" }, { status: 404 });
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: error instanceof Error ? error.message : "删除聊天记录失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1253,9 +1253,9 @@ export default function Home() {
|
||||
setArchivedSessionIds((current) => current.filter((id) => id !== session.id));
|
||||
if (activeSessionId === session.id) setActiveSessionId(nextSessions[0]?.id ?? "");
|
||||
try {
|
||||
const supabase = createBrowserSupabaseClient();
|
||||
const { error } = await supabase.from("chat_sessions").delete().eq("id", session.id).eq("user_id", account.user.id);
|
||||
if (error) throw error;
|
||||
const response = await fetch(`/api/sessions/${encodeURIComponent(session.id)}`, { method: "DELETE" });
|
||||
const payload = await response.json().catch(() => null) as { error?: string } | null;
|
||||
if (!response.ok) throw new Error(payload?.error || "删除聊天记录失败");
|
||||
} catch (caught) {
|
||||
setSessions(previousSessions);
|
||||
setComposerNotice(caught instanceof Error ? `删除失败:${caught.message}` : "删除失败");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
grant delete on table public.chat_sessions to authenticated;
|
||||
|
||||
commit;
|
||||
@@ -3,10 +3,13 @@ from pathlib import Path
|
||||
|
||||
PAGE = Path("frontend/src/app/page.tsx")
|
||||
STYLES = Path("frontend/src/app/globals.css")
|
||||
SESSION_ROW = Path("frontend/src/components/sidebar-session-row.tsx")
|
||||
SESSION_DELETE_ROUTE = Path("frontend/src/app/api/sessions/[id]/route.ts")
|
||||
SESSION_DELETE_MIGRATION = Path("frontend/supabase/migrations/20260721100000_chat_sessions_delete_grant.sql")
|
||||
|
||||
|
||||
def test_chat_history_management_actions_are_exposed() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8") + STYLES.read_text(encoding="utf-8")
|
||||
source = PAGE.read_text(encoding="utf-8") + STYLES.read_text(encoding="utf-8") + SESSION_ROW.read_text(encoding="utf-8")
|
||||
for expected in (
|
||||
"renameSession",
|
||||
"deleteSession",
|
||||
@@ -28,5 +31,15 @@ def test_chat_history_management_actions_are_exposed() -> None:
|
||||
"恢复",
|
||||
"删除",
|
||||
"转发",
|
||||
'fetch(`/api/sessions/${encodeURIComponent(session.id)}`',
|
||||
):
|
||||
assert expected in source
|
||||
|
||||
|
||||
def test_chat_session_delete_is_server_controlled_and_granted() -> None:
|
||||
route = SESSION_DELETE_ROUTE.read_text(encoding="utf-8")
|
||||
migration = SESSION_DELETE_MIGRATION.read_text(encoding="utf-8")
|
||||
assert 'from("chat_sessions")' in route
|
||||
assert '.eq("user_id", user.id)' in route
|
||||
assert 'count !== 1' in route
|
||||
assert 'grant delete on table public.chat_sessions to authenticated' in migration.lower()
|
||||
|
||||
Reference in New Issue
Block a user