mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-02-12 15:52:47 +00:00
feat(api): add user flagging functionality
This commit is contained in:
@@ -502,8 +502,14 @@
|
||||
"admin": "Admins",
|
||||
"user": "Users",
|
||||
"verified": "Verified",
|
||||
"unverified": "Unverified"
|
||||
}
|
||||
"unverified": "Unverified",
|
||||
"flagged": "Flagged"
|
||||
},
|
||||
"flag": "Flag User",
|
||||
"unflag": "Unflag User",
|
||||
"flagged": "User flagged",
|
||||
"unflagged": "User unflagged",
|
||||
"flagFailed": "Failed to update flag status"
|
||||
},
|
||||
"categories": {
|
||||
"title": "Category Management",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "DelistReason" ADD VALUE 'UNUSUAL_ACTIVITY';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "flagged" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "users" ADD COLUMN "flaggedAt" TIMESTAMP(3);
|
||||
ALTER TABLE "users" ADD COLUMN "flaggedReason" TEXT;
|
||||
@@ -23,6 +23,9 @@ model User {
|
||||
githubUsername String?
|
||||
apiKey String? @unique
|
||||
mcpPromptsPublicByDefault Boolean @default(false)
|
||||
flagged Boolean @default(false)
|
||||
flaggedAt DateTime?
|
||||
flaggedReason String?
|
||||
accounts Account[]
|
||||
subscriptions CategorySubscription[]
|
||||
changeRequests ChangeRequest[]
|
||||
@@ -408,4 +411,5 @@ enum DelistReason {
|
||||
LOW_QUALITY
|
||||
NOT_LLM_INSTRUCTION
|
||||
MANUAL
|
||||
UNUSUAL_ACTIVITY
|
||||
}
|
||||
|
||||
@@ -15,10 +15,16 @@ export async function PATCH(
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { role, verified } = body;
|
||||
const { role, verified, flagged, flaggedReason } = body;
|
||||
|
||||
// Build update data
|
||||
const updateData: { role?: "ADMIN" | "USER"; verified?: boolean } = {};
|
||||
const updateData: {
|
||||
role?: "ADMIN" | "USER";
|
||||
verified?: boolean;
|
||||
flagged?: boolean;
|
||||
flaggedAt?: Date | null;
|
||||
flaggedReason?: string | null;
|
||||
} = {};
|
||||
|
||||
if (role !== undefined) {
|
||||
if (!["ADMIN", "USER"].includes(role)) {
|
||||
@@ -31,9 +37,33 @@ export async function PATCH(
|
||||
updateData.verified = verified;
|
||||
}
|
||||
|
||||
if (flagged !== undefined) {
|
||||
updateData.flagged = flagged;
|
||||
if (flagged) {
|
||||
updateData.flaggedAt = new Date();
|
||||
updateData.flaggedReason = flaggedReason || null;
|
||||
} else {
|
||||
updateData.flaggedAt = null;
|
||||
updateData.flaggedReason = null;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await db.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
role: true,
|
||||
verified: true,
|
||||
flagged: true,
|
||||
flaggedAt: true,
|
||||
flaggedReason: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(user);
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function GET(request: NextRequest) {
|
||||
OR?: Array<{ email?: { contains: string; mode: "insensitive" }; username?: { contains: string; mode: "insensitive" }; name?: { contains: string; mode: "insensitive" } }>;
|
||||
role?: "ADMIN" | "USER";
|
||||
verified?: boolean;
|
||||
flagged?: boolean;
|
||||
};
|
||||
|
||||
const filterConditions: WhereCondition = {};
|
||||
@@ -58,6 +59,9 @@ export async function GET(request: NextRequest) {
|
||||
case "unverified":
|
||||
filterConditions.verified = false;
|
||||
break;
|
||||
case "flagged":
|
||||
filterConditions.flagged = true;
|
||||
break;
|
||||
default:
|
||||
// "all" - no filter
|
||||
break;
|
||||
@@ -95,6 +99,9 @@ export async function GET(request: NextRequest) {
|
||||
avatar: true,
|
||||
role: true,
|
||||
verified: true,
|
||||
flagged: true,
|
||||
flaggedAt: true,
|
||||
flaggedReason: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
|
||||
@@ -48,6 +48,13 @@ export async function POST(request: Request) {
|
||||
|
||||
const { title, description, content, type, structuredFormat, categoryId, tagIds, contributorIds, isPrivate, mediaUrl, requiresMediaUpload, requiredMediaType, requiredMediaCount } = parsed.data;
|
||||
|
||||
// Check if user is flagged (for auto-delisting)
|
||||
const currentUser = await db.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { flagged: true },
|
||||
});
|
||||
const isUserFlagged = currentUser?.flagged ?? false;
|
||||
|
||||
// Rate limit: Check if user created a prompt in the last 30 seconds
|
||||
const thirtySecondsAgo = new Date(Date.now() - 30 * 1000);
|
||||
const recentPrompt = await db.prompt.findFirst({
|
||||
@@ -135,6 +142,7 @@ export async function POST(request: Request) {
|
||||
const slug = await generatePromptSlug(title);
|
||||
|
||||
// Create prompt with tags
|
||||
// Auto-delist if user is flagged
|
||||
const prompt = await db.prompt.create({
|
||||
data: {
|
||||
title,
|
||||
@@ -150,6 +158,12 @@ export async function POST(request: Request) {
|
||||
requiredMediaCount: requiresMediaUpload ? requiredMediaCount : null,
|
||||
authorId: session.user.id,
|
||||
categoryId: categoryId || null,
|
||||
// Auto-delist prompts from flagged users
|
||||
...(isUserFlagged && {
|
||||
isUnlisted: true,
|
||||
unlistedAt: new Date(),
|
||||
delistReason: "UNUSUAL_ACTIVITY",
|
||||
}),
|
||||
tags: {
|
||||
create: tagIds.map((tagId) => ({
|
||||
tagId,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import { formatDistanceToNow } from "@/lib/date";
|
||||
import { MoreHorizontal, Shield, User, Trash2, BadgeCheck, Search, Loader2, ChevronLeft, ChevronRight, Filter } from "lucide-react";
|
||||
import { MoreHorizontal, Shield, User, Trash2, BadgeCheck, Search, Loader2, ChevronLeft, ChevronRight, Filter, Flag, AlertTriangle } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -51,6 +51,9 @@ interface UserData {
|
||||
avatar: string | null;
|
||||
role: "ADMIN" | "USER";
|
||||
verified: boolean;
|
||||
flagged: boolean;
|
||||
flaggedAt: string | null;
|
||||
flaggedReason: string | null;
|
||||
createdAt: string;
|
||||
_count: {
|
||||
prompts: number;
|
||||
@@ -148,12 +151,31 @@ export function UsersTable() {
|
||||
if (!res.ok) throw new Error("Failed to update verification");
|
||||
|
||||
toast.success(verified ? t("verified") : t("unverified"));
|
||||
fetchUsers(currentPage, searchQuery, userFilter);
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast.error(t("verifyFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlagToggle = async (userId: string, flagged: boolean) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${userId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ flagged, flaggedReason: flagged ? "Unusual activity" : null }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to update flag status");
|
||||
|
||||
toast.success(flagged ? t("flagged") : t("unflagged"));
|
||||
fetchUsers(currentPage, searchQuery, userFilter);
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast.error(t("flagFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteUserId) return;
|
||||
|
||||
@@ -195,6 +217,7 @@ export function UsersTable() {
|
||||
<SelectItem value="user">{t("filters.user")}</SelectItem>
|
||||
<SelectItem value="verified">{t("filters.verified")}</SelectItem>
|
||||
<SelectItem value="unverified">{t("filters.unverified")}</SelectItem>
|
||||
<SelectItem value="flagged">{t("filters.flagged")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2">
|
||||
@@ -241,6 +264,7 @@ export function UsersTable() {
|
||||
<div className="font-medium flex items-center gap-1 truncate">
|
||||
{user.name || user.username}
|
||||
{user.verified && <BadgeCheck className="h-4 w-4 text-blue-500 flex-shrink-0" />}
|
||||
{user.flagged && <AlertTriangle className="h-4 w-4 text-amber-500 flex-shrink-0" />}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">@{user.username}</div>
|
||||
</div>
|
||||
@@ -267,6 +291,10 @@ export function UsersTable() {
|
||||
<BadgeCheck className="h-4 w-4 mr-2" />
|
||||
{user.verified ? t("unverify") : t("verify")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleFlagToggle(user.id, !user.flagged)}>
|
||||
<Flag className="h-4 w-4 mr-2" />
|
||||
{user.flagged ? t("unflag") : t("flag")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
@@ -321,6 +349,7 @@ export function UsersTable() {
|
||||
<div className="font-medium flex items-center gap-1">
|
||||
{user.name || user.username}
|
||||
{user.verified && <BadgeCheck className="h-4 w-4 text-blue-500" />}
|
||||
{user.flagged && <AlertTriangle className="h-4 w-4 text-amber-500" />}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">@{user.username}</div>
|
||||
</div>
|
||||
@@ -360,6 +389,10 @@ export function UsersTable() {
|
||||
<BadgeCheck className="h-4 w-4 mr-2" />
|
||||
{user.verified ? t("unverify") : t("verify")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleFlagToggle(user.id, !user.flagged)}>
|
||||
<Flag className="h-4 w-4 mr-2" />
|
||||
{user.flagged ? t("unflag") : t("flag")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
|
||||
Reference in New Issue
Block a user