feat(api): implement soft delete for prompts
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "prompts" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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' 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 "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.
|
||||
</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'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>
|
||||
);
|
||||
}
|
||||
@@ -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>© {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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user