feat: add privacy retention controls

This commit is contained in:
2026-07-19 05:52:48 +08:00
parent c6079bbb47
commit 2af99a4b43
6 changed files with 30 additions and 2 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
import { db } from "@/lib/db";
export default async function Account({ searchParams }: { searchParams: Promise<{ error?: string; updated?: string }> }) {
const user = await getSession(); if (!user) redirect("/login"); const query = await searchParams;
return <><h1></h1>{query.error && <p className="error">{query.error}</p>}{query.updated && <p></p>}<section className="card"><p className="meta">{user.username}</p><h2></h2><form action="/api/auth/password" method="post"><label><input name="currentPassword" type="password" autoComplete="current-password" required /></label><label><input name="newPassword" type="password" autoComplete="new-password" minLength={10} required /></label><label><input name="confirmPassword" type="password" autoComplete="new-password" minLength={10} required /></label><button></button></form></section></>;
const ownedSources = db.prepare("SELECT count(*) AS count FROM sources WHERE user_id=?").get(user.id) as { count: number };
return <><h1></h1>{query.error && <p className="error">{query.error}</p>}{query.updated && <p></p>}<section className="card"><p className="meta">{user.username}</p><h2></h2><form action="/api/auth/password" method="post"><label><input name="currentPassword" type="password" autoComplete="current-password" required /></label><label><input name="newPassword" type="password" autoComplete="new-password" minLength={10} required /></label><label><input name="confirmPassword" type="password" autoComplete="new-password" minLength={10} required /></label><button></button></form></section><section className="card"><h2></h2>{ownedSources.count ? <p className="error"> {ownedSources.count} </p> : <form action="/api/auth/delete" method="post"><p className="meta"> Hub </p><label><input name="currentPassword" type="password" autoComplete="current-password" required /></label><label> DELETE <input name="confirmation" required /></label><button className="danger"></button></form>}</section></>;
}
+8
View File
@@ -0,0 +1,8 @@
import bcrypt from "bcryptjs";
import { NextResponse } from "next/server";
import { clearSession, requireUser } from "@/lib/auth";
import { audit } from "@/lib/audit";
import { db } from "@/lib/db";
import { externalUrl } from "@/lib/http";
import { requireSameOrigin } from "@/lib/security";
export async function POST(request: Request) { try { requireSameOrigin(request); const user = await requireUser(); if (user.role === "admin") throw new Error("管理員帳號不可自行刪除"); const form = await request.formData(); if (String(form.get("confirmation")) !== "DELETE") throw new Error("請輸入 DELETE 確認"); const account = db.prepare("SELECT password_hash FROM users WHERE id=?").get(user.id) as { password_hash: string } | undefined; if (!account || !(await bcrypt.compare(String(form.get("currentPassword") || ""), account.password_hash))) throw new Error("目前密碼不正確"); const owned = db.prepare("SELECT count(*) AS count FROM sources WHERE user_id=?").get(user.id) as { count: number }; if (owned.count) throw new Error("請先處理你建立的來源"); audit(user.id, "account.delete", "user", user.id); db.prepare("DELETE FROM users WHERE id=?").run(user.id); await clearSession(); return NextResponse.redirect(externalUrl(request, "/?account=deleted")); } catch (error) { return NextResponse.redirect(externalUrl(request, "/account?error=" + encodeURIComponent(error instanceof Error ? error.message : "delete"))); } }