From e176a2eedb7ea0cce3ae65340dae0136363cf686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Kadir=20Ak=C4=B1n?= Date: Thu, 11 Dec 2025 15:20:46 +0300 Subject: [PATCH] feat(api): implement soft delete for prompts --- .../migration.sql | 2 + prisma/schema.prisma | 1 + src/app/api/prompts/[id]/route.ts | 28 ++-- src/app/api/prompts/route.ts | 1 + src/app/layout.tsx | 2 +- src/app/page.tsx | 26 +++- src/app/privacy/page.tsx | 110 +++++++++++++++ src/app/prompts/[id]/page.tsx | 4 +- src/app/terms/page.tsx | 130 ++++++++++++++++++ src/components/layout/footer.tsx | 14 +- src/components/layout/header.tsx | 14 +- src/lib/ai/embeddings.ts | 4 +- 12 files changed, 311 insertions(+), 25 deletions(-) create mode 100644 prisma/migrations/20251211114705_add_soft_delete_to_prompts/migration.sql create mode 100644 src/app/privacy/page.tsx create mode 100644 src/app/terms/page.tsx diff --git a/prisma/migrations/20251211114705_add_soft_delete_to_prompts/migration.sql b/prisma/migrations/20251211114705_add_soft_delete_to_prompts/migration.sql new file mode 100644 index 00000000..b0e1f4df --- /dev/null +++ b/prisma/migrations/20251211114705_add_soft_delete_to_prompts/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "prompts" ADD COLUMN "deletedAt" TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c6ceea4b..02a41c9a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/app/api/prompts/[id]/route.ts b/src/app/api/prompts/[id]/route.ts index 533afa6d..67ff6fac 100644 --- a/src/app/api/prompts/[id]/route.ts +++ b/src/app/api/prompts/[id]/route.ts @@ -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( diff --git a/src/app/api/prompts/route.ts b/src/app/api/prompts/route.ts index 95fcdc6c..748f5f6e 100644 --- a/src/app/api/prompts/route.ts +++ b/src/app/api/prompts/route.ts @@ -138,6 +138,7 @@ export async function GET(request: Request) { const where: Record = { isPrivate: false, + deletedAt: null, // Exclude soft-deleted prompts }; if (type) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f25f7aa4..02f2917b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -95,7 +95,7 @@ export default async function RootLayout({
-
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 6d2d08b9..be65ac90 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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() { - + {showRegisterButton && ( + + )}
@@ -234,9 +244,13 @@ export default async function HomePage() {

{tHomepage("freeAndOpen")}

- + {showRegisterButton && ( + + )} diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx new file mode 100644 index 00000000..bc4e23a6 --- /dev/null +++ b/src/app/privacy/page.tsx @@ -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 ( +
+

Privacy Policy

+ +
+

+ Last updated: {new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })} +

+ +
+

Overview

+

+ prompts.chat is an open-source platform for collecting, organizing, and sharing AI prompts. + All prompts shared on this platform are released under the{" "} + + CC0 (Creative Commons Zero) + {" "} + license, meaning they are in the public domain. +

+
+ +
+

Information We Collect

+

+ When you create an account, we collect: +

+
    +
  • Email address (for authentication)
  • +
  • Username and display name
  • +
  • Profile information you choose to provide
  • +
+

+ When you use the platform, we collect: +

+
    +
  • Prompts you create and share
  • +
  • Categories and tags you assign to prompts
  • +
  • Basic usage analytics to improve the service
  • +
+
+ +
+

How We Use Your Information

+

+ We use collected information to: +

+
    +
  • Provide and maintain the service
  • +
  • Associate prompts with your account
  • +
  • Enable discovery of prompts by other users
  • +
  • Improve the platform based on usage patterns
  • +
+
+ +
+

Data Sharing

+

+ 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. +

+
+ +
+

Cookies

+

+ We use essential cookies for authentication and session management. We may use + analytics cookies to understand how the service is used. +

+
+ +
+

Your Rights

+

+ You can: +

+
    +
  • Access and update your account information
  • +
  • Delete your account and associated data
  • +
  • Export your prompts
  • +
+
+ +
+

Open Source

+

+ This platform is open source. You can review the code and self-host your own instance at{" "} + + GitHub + . +

+
+ +
+

Contact

+

+ For privacy-related inquiries, please open an issue on our GitHub repository. +

+
+
+
+ ); +} diff --git a/src/app/prompts/[id]/page.tsx b/src/app/prompts/[id]/page.tsx index c669ded8..3c9c79c7 100644 --- a/src/app/prompts/[id]/page.tsx +++ b/src/app/prompts/[id]/page.tsx @@ -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: { diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx new file mode 100644 index 00000000..4a7dc8e0 --- /dev/null +++ b/src/app/terms/page.tsx @@ -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 ( +
+

Terms of Service

+ +
+

+ Last updated: {new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })} +

+ +
+

Acceptance of Terms

+

+ 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. +

+
+ +
+

Description of Service

+

+ 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. +

+
+ +
+

CC0 License

+

+ All prompts shared on this platform are released under the{" "} + + CC0 1.0 Universal (CC0 1.0) Public Domain Dedication + . + By submitting a prompt, you waive all copyright and related rights to the extent possible under law. + This means: +

+
    +
  • Anyone can use, modify, and distribute your prompts without attribution
  • +
  • Your prompts become part of the public domain
  • +
  • You cannot later revoke this dedication
  • +
+
+ +
+

User Accounts

+

+ To submit prompts, you must create an account. You are responsible for: +

+
    +
  • Maintaining the security of your account credentials
  • +
  • All activities that occur under your account
  • +
  • Providing accurate account information
  • +
+
+ +
+

Acceptable Use

+

+ You agree not to: +

+
    +
  • Submit prompts designed to generate harmful, illegal, or abusive content
  • +
  • Impersonate others or misrepresent your affiliation
  • +
  • Attempt to gain unauthorized access to the service
  • +
  • Use the service to spam or harass others
  • +
  • Submit content that infringes on others' intellectual property rights
  • +
+
+ +
+

Content Moderation

+

+ 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. +

+
+ +
+

Disclaimer of Warranties

+

+ The service is provided "as is" 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. +

+
+ +
+

Limitation of Liability

+

+ 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. +

+
+ +
+

Changes to Terms

+

+ We may update these terms from time to time. Continued use of the service after changes + constitutes acceptance of the new terms. +

+
+ +
+

Open Source

+

+ This platform is open source and available at{" "} + + GitHub + . + You are free to self-host your own instance under the project's license. +

+
+ +
+

Contact

+

+ For questions about these terms, please open an issue on our GitHub repository. +

+
+
+
+ ); +} diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index fbbec554..ab46aa0b 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -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 (
- © {new Date().getFullYear()} {branding.name} + + + CC0 + {" "}{new Date().getFullYear()} {branding.name} +
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 0f79d001..b6e4d423 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -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) { - {showRegister && ( + {showRegisterButton && ( )} diff --git a/src/lib/ai/embeddings.ts b/src/lib/ai/embeddings.ts index a648329f..8ce909d7 100644 --- a/src/lib/ai/embeddings.ts +++ b/src/lib/ai/embeddings.ts @@ -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: {