feat(api): implement soft delete for prompts

This commit is contained in:
Fatih Kadir Akın
2025-12-11 15:20:46 +03:00
parent d73149427b
commit e176a2eedb
12 changed files with 311 additions and 25 deletions
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "prompts" ADD COLUMN "deletedAt" TIMESTAMP(3);
+1
View File
@@ -136,6 +136,7 @@ model Prompt {
viewCount Int @default(0)
isFeatured Boolean @default(false)
featuredAt DateTime?
deletedAt DateTime? // Soft delete - CC0 prompts cannot be permanently deleted
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+19 -9
View File
@@ -52,7 +52,7 @@ export async function GET(
},
});
if (!prompt) {
if (!prompt || prompt.deletedAt) {
return NextResponse.json(
{ error: "not_found", message: "Prompt not found" },
{ status: 404 }
@@ -194,7 +194,7 @@ export async function PATCH(
}
}
// Delete prompt
// Soft delete prompt (admin only - CC0 prompts cannot be deleted by users)
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
@@ -210,10 +210,18 @@ export async function DELETE(
);
}
// Check if prompt exists and user owns it
// Only admins can soft-delete prompts (CC0 content is public domain)
if (session.user.role !== "ADMIN") {
return NextResponse.json(
{ error: "forbidden", message: "Prompts are released under CC0 and cannot be deleted. Contact an admin if there is an issue." },
{ status: 403 }
);
}
// Check if prompt exists
const existing = await db.prompt.findUnique({
where: { id },
select: { authorId: true },
select: { id: true, deletedAt: true },
});
if (!existing) {
@@ -223,18 +231,20 @@ export async function DELETE(
);
}
if (existing.authorId !== session.user.id && session.user.role !== "ADMIN") {
if (existing.deletedAt) {
return NextResponse.json(
{ error: "forbidden", message: "You can only delete your own prompts" },
{ status: 403 }
{ error: "already_deleted", message: "Prompt is already deleted" },
{ status: 400 }
);
}
await db.prompt.delete({
// Soft delete by setting deletedAt timestamp
await db.prompt.update({
where: { id },
data: { deletedAt: new Date() },
});
return NextResponse.json({ success: true });
return NextResponse.json({ success: true, message: "Prompt soft deleted" });
} catch (error) {
console.error("Delete prompt error:", error);
return NextResponse.json(
+1
View File
@@ -138,6 +138,7 @@ export async function GET(request: Request) {
const where: Record<string, unknown> = {
isPrivate: false,
deletedAt: null, // Exclude soft-deleted prompts
};
if (type) {
+1 -1
View File
@@ -95,7 +95,7 @@ export default async function RootLayout({
<body className={`${fontClasses} antialiased`}>
<Providers locale={locale} messages={messages} theme={config.theme} branding={config.branding}>
<div className="relative min-h-screen flex flex-col">
<Header showRegister={config.auth.provider === "credentials" && config.auth.allowRegistration} />
<Header authProvider={config.auth.provider} allowRegistration={config.auth.allowRegistration} />
<main className="flex-1">{children}</main>
<Footer />
</div>
+20 -6
View File
@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server";
import { ArrowRight, Bell, FolderOpen, Sparkles } from "lucide-react";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { getConfig } from "@/lib/config";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PromptList } from "@/components/prompts/prompt-list";
@@ -14,6 +15,11 @@ export default async function HomePage() {
const tHomepage = await getTranslations("homepage");
const tNav = await getTranslations("nav");
const session = await auth();
const config = await getConfig();
const isOAuth = config.auth.provider !== "credentials";
// Show register button for OAuth (with login text) or credentials with registration enabled
const showRegisterButton = isOAuth || (config.auth.provider === "credentials" && config.auth.allowRegistration);
// For logged-in users, show subscribed categories feed
if (session?.user) {
@@ -182,9 +188,13 @@ export default async function HomePage() {
<ArrowRight className="ml-1.5 h-4 w-4" />
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/register">{tNav("register")}</Link>
</Button>
{showRegisterButton && (
<Button variant="outline" asChild>
<Link href={isOAuth ? "/login" : "/register"}>
{isOAuth ? tNav("login") : tNav("register")}
</Link>
</Button>
)}
</div>
</div>
<div className="hidden md:block">
@@ -234,9 +244,13 @@ export default async function HomePage() {
<p className="text-sm text-muted-foreground">{tHomepage("freeAndOpen")}</p>
</div>
</div>
<Button asChild>
<Link href="/register">{tHomepage("createAccount")}</Link>
</Button>
{showRegisterButton && (
<Button asChild>
<Link href={isOAuth ? "/login" : "/register"}>
{isOAuth ? tNav("login") : tHomepage("createAccount")}
</Link>
</Button>
)}
</div>
</div>
</section>
+110
View File
@@ -0,0 +1,110 @@
import Link from "next/link";
export const metadata = {
title: "Privacy Policy - prompts.chat",
description: "Privacy Policy for prompts.chat",
};
export default function PrivacyPage() {
return (
<div className="container max-w-3xl py-10">
<h1 className="text-2xl font-bold mb-6">Privacy Policy</h1>
<div className="prose prose-neutral dark:prose-invert max-w-none space-y-6">
<p className="text-muted-foreground">
Last updated: {new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
</p>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Overview</h2>
<p className="text-muted-foreground">
prompts.chat is an open-source platform for collecting, organizing, and sharing AI prompts.
All prompts shared on this platform are released under the{" "}
<Link href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">
CC0 (Creative Commons Zero)
</Link>{" "}
license, meaning they are in the public domain.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Information We Collect</h2>
<p className="text-muted-foreground">
When you create an account, we collect:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Email address (for authentication)</li>
<li>Username and display name</li>
<li>Profile information you choose to provide</li>
</ul>
<p className="text-muted-foreground">
When you use the platform, we collect:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Prompts you create and share</li>
<li>Categories and tags you assign to prompts</li>
<li>Basic usage analytics to improve the service</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">How We Use Your Information</h2>
<p className="text-muted-foreground">
We use collected information to:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Provide and maintain the service</li>
<li>Associate prompts with your account</li>
<li>Enable discovery of prompts by other users</li>
<li>Improve the platform based on usage patterns</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Data Sharing</h2>
<p className="text-muted-foreground">
Prompts you share are public and released under CC0. Your username will be displayed
alongside your prompts. We do not sell your personal information to third parties.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Cookies</h2>
<p className="text-muted-foreground">
We use essential cookies for authentication and session management. We may use
analytics cookies to understand how the service is used.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Your Rights</h2>
<p className="text-muted-foreground">
You can:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Access and update your account information</li>
<li>Delete your account and associated data</li>
<li>Export your prompts</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Open Source</h2>
<p className="text-muted-foreground">
This platform is open source. You can review the code and self-host your own instance at{" "}
<Link href="https://github.com/f/awesome-chatgpt-prompts" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">
GitHub
</Link>.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Contact</h2>
<p className="text-muted-foreground">
For privacy-related inquiries, please open an issue on our GitHub repository.
</p>
</section>
</div>
</div>
);
}
+2 -2
View File
@@ -47,8 +47,8 @@ export default async function PromptPage({ params }: PromptPageProps) {
const t = await getTranslations("prompts");
const locale = await getLocale();
const prompt = await db.prompt.findUnique({
where: { id },
const prompt = await db.prompt.findFirst({
where: { id, deletedAt: null },
include: {
author: {
select: {
+130
View File
@@ -0,0 +1,130 @@
import Link from "next/link";
export const metadata = {
title: "Terms of Service - prompts.chat",
description: "Terms of Service for prompts.chat",
};
export default function TermsPage() {
return (
<div className="container max-w-3xl py-10">
<h1 className="text-2xl font-bold mb-6">Terms of Service</h1>
<div className="prose prose-neutral dark:prose-invert max-w-none space-y-6">
<p className="text-muted-foreground">
Last updated: {new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
</p>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Acceptance of Terms</h2>
<p className="text-muted-foreground">
By accessing and using prompts.chat, you agree to be bound by these Terms of Service.
If you do not agree to these terms, please do not use the service.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Description of Service</h2>
<p className="text-muted-foreground">
prompts.chat is an open-source platform for collecting, organizing, and sharing AI prompts.
The service allows users to create, discover, and share prompts for use with AI language models.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">CC0 License</h2>
<p className="text-muted-foreground">
All prompts shared on this platform are released under the{" "}
<Link href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
</Link>.
By submitting a prompt, you waive all copyright and related rights to the extent possible under law.
This means:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Anyone can use, modify, and distribute your prompts without attribution</li>
<li>Your prompts become part of the public domain</li>
<li>You cannot later revoke this dedication</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">User Accounts</h2>
<p className="text-muted-foreground">
To submit prompts, you must create an account. You are responsible for:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Maintaining the security of your account credentials</li>
<li>All activities that occur under your account</li>
<li>Providing accurate account information</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Acceptable Use</h2>
<p className="text-muted-foreground">
You agree not to:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1">
<li>Submit prompts designed to generate harmful, illegal, or abusive content</li>
<li>Impersonate others or misrepresent your affiliation</li>
<li>Attempt to gain unauthorized access to the service</li>
<li>Use the service to spam or harass others</li>
<li>Submit content that infringes on others&apos; intellectual property rights</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Content Moderation</h2>
<p className="text-muted-foreground">
We reserve the right to remove any content that violates these terms or that we deem
inappropriate. We may also suspend or terminate accounts that violate these terms.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Disclaimer of Warranties</h2>
<p className="text-muted-foreground">
The service is provided &quot;as is&quot; without warranties of any kind. We do not guarantee
that prompts will produce specific results when used with AI models. The effectiveness
of prompts may vary depending on the AI model and context.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Limitation of Liability</h2>
<p className="text-muted-foreground">
We shall not be liable for any indirect, incidental, special, or consequential damages
arising from your use of the service or any prompts obtained through the service.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Changes to Terms</h2>
<p className="text-muted-foreground">
We may update these terms from time to time. Continued use of the service after changes
constitutes acceptance of the new terms.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Open Source</h2>
<p className="text-muted-foreground">
This platform is open source and available at{" "}
<Link href="https://github.com/f/awesome-chatgpt-prompts" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">
GitHub
</Link>.
You are free to self-host your own instance under the project&apos;s license.
</p>
</section>
<section className="space-y-3">
<h2 className="text-lg font-semibold">Contact</h2>
<p className="text-muted-foreground">
For questions about these terms, please open an issue on our GitHub repository.
</p>
</section>
</div>
</div>
);
}
+12 -2
View File
@@ -1,5 +1,6 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useBranding } from "@/components/providers/branding-provider";
@@ -9,11 +10,20 @@ export function Footer() {
return (
<footer className="border-t">
<div className="container flex items-center justify-between h-10 text-xs text-muted-foreground">
<span>&copy; {new Date().getFullYear()} {branding.name}</span>
<span className="flex items-center gap-1.5">
<Image src="/logo.svg" alt="" width={14} height={14} className="dark:invert" />
<Link href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">CC0</Link>
{" "}{new Date().getFullYear()} {branding.name}
</span>
<nav className="flex items-center gap-4">
<Link href="/privacy" className="hover:text-foreground">Privacy</Link>
<Link href="/terms" className="hover:text-foreground">Terms</Link>
<Link href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-foreground">GitHub</Link>
<Link href="https://github.com/f/awesome-chatgpt-prompts" target="_blank" rel="noopener noreferrer" className="hover:text-foreground flex items-center gap-1">
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
</svg>
GitHub
</Link>
</nav>
</div>
</footer>
+10 -4
View File
@@ -44,10 +44,14 @@ const languages = [
];
interface HeaderProps {
showRegister?: boolean;
authProvider?: string;
allowRegistration?: boolean;
}
export function Header({ showRegister = true }: HeaderProps) {
export function Header({ authProvider = "credentials", allowRegistration = true }: HeaderProps) {
const isOAuth = authProvider !== "credentials";
// Show register button for OAuth (with login text) or credentials with registration enabled
const showRegisterButton = isOAuth || (authProvider === "credentials" && allowRegistration);
const { data: session } = useSession();
const t = useTranslations();
const { theme, setTheme } = useTheme();
@@ -300,9 +304,11 @@ export function Header({ showRegister = true }: HeaderProps) {
<Button variant="ghost" size="sm" className="h-8 text-xs" asChild>
<Link href="/login">{t("nav.login")}</Link>
</Button>
{showRegister && (
{showRegisterButton && (
<Button size="sm" className="h-8 text-xs" asChild>
<Link href="/register">{t("nav.register")}</Link>
<Link href={isOAuth ? "/login" : "/register"}>
{isOAuth ? t("nav.login") : t("nav.register")}
</Link>
</Button>
)}
</div>
+3 -1
View File
@@ -70,6 +70,7 @@ export async function generateAllEmbeddings(): Promise<{ success: number; failed
where: {
embedding: { equals: Prisma.DbNull },
isPrivate: false,
deletedAt: null,
},
select: { id: true },
});
@@ -148,10 +149,11 @@ export async function semanticSearch(
// Generate embedding for the query
const queryEmbedding = await generateEmbedding(query);
// Fetch all public prompts with embeddings
// Fetch all public prompts with embeddings (excluding soft-deleted)
const prompts = await db.prompt.findMany({
where: {
isPrivate: false,
deletedAt: null,
embedding: { not: Prisma.DbNull },
},
select: {