mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-03-03 02:47:02 +00:00
feat(api): add daily generation credits reset cron job and user settings
This commit is contained in:
@@ -43,6 +43,9 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production"
|
||||
# Logging (optional)
|
||||
# LOG_LEVEL="info" # Options: trace, debug, info, warn, error, fatal
|
||||
|
||||
# Cron Job Secret (for daily credit reset)
|
||||
CRON_SECRET="IkD/VzyrCRc6c/146TjKhIzOZ9HFq+Meo00y+wQpws8="
|
||||
|
||||
# Media Generation - Wiro.ai (optional)
|
||||
# WIRO_API_KEY=your_wiro_api_key
|
||||
# WIRO_VIDEO_MODELS="google/veo3.1-fast" # Comma-separated list of video models
|
||||
|
||||
29
.github/workflows/reset-credits.yml
vendored
Normal file
29
.github/workflows/reset-credits.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Reset Daily Generation Credits
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at midnight UTC every day
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
reset-credits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Reset daily generation credits
|
||||
run: |
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
"https://prompts.chat/api/cron/reset-credits" \
|
||||
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Response: $body"
|
||||
echo "HTTP Code: $http_code"
|
||||
|
||||
if [ "$http_code" != "200" ]; then
|
||||
echo "Failed to reset credits"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Credits reset successfully"
|
||||
@@ -547,7 +547,15 @@
|
||||
"unflag": "Unflag User",
|
||||
"flagged": "User flagged",
|
||||
"unflagged": "User unflagged",
|
||||
"flagFailed": "Failed to update flag status"
|
||||
"flagFailed": "Failed to update flag status",
|
||||
"editCredits": "Edit Credits",
|
||||
"editCreditsTitle": "Edit Generation Credits",
|
||||
"editCreditsDescription": "Set daily generation credit limit for @{username}",
|
||||
"dailyLimit": "Daily Credit Limit",
|
||||
"currentCredits": "Currently: {remaining}/{limit} credits remaining",
|
||||
"creditsUpdated": "Credits updated successfully",
|
||||
"creditsUpdateFailed": "Failed to update credits",
|
||||
"save": "Save"
|
||||
},
|
||||
"categories": {
|
||||
"title": "Category Management",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "dailyGenerationLimit" INTEGER NOT NULL DEFAULT 10;
|
||||
ALTER TABLE "users" ADD COLUMN "generationCreditsRemaining" INTEGER NOT NULL DEFAULT 10;
|
||||
ALTER TABLE "users" ADD COLUMN "generationCreditsResetAt" TIMESTAMP(3);
|
||||
@@ -26,6 +26,9 @@ model User {
|
||||
flagged Boolean @default(false)
|
||||
flaggedAt DateTime?
|
||||
flaggedReason String?
|
||||
dailyGenerationLimit Int @default(10)
|
||||
generationCreditsRemaining Int @default(10)
|
||||
generationCreditsResetAt DateTime?
|
||||
accounts Account[]
|
||||
subscriptions CategorySubscription[]
|
||||
changeRequests ChangeRequest[]
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function PATCH(
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { role, verified, flagged, flaggedReason } = body;
|
||||
const { role, verified, flagged, flaggedReason, dailyGenerationLimit } = body;
|
||||
|
||||
// Build update data
|
||||
const updateData: {
|
||||
@@ -24,6 +24,8 @@ export async function PATCH(
|
||||
flagged?: boolean;
|
||||
flaggedAt?: Date | null;
|
||||
flaggedReason?: string | null;
|
||||
dailyGenerationLimit?: number;
|
||||
generationCreditsRemaining?: number;
|
||||
} = {};
|
||||
|
||||
if (role !== undefined) {
|
||||
@@ -48,6 +50,16 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
if (dailyGenerationLimit !== undefined) {
|
||||
const limit = parseInt(dailyGenerationLimit, 10);
|
||||
if (isNaN(limit) || limit < 0) {
|
||||
return NextResponse.json({ error: "Invalid daily generation limit" }, { status: 400 });
|
||||
}
|
||||
updateData.dailyGenerationLimit = limit;
|
||||
// Also reset remaining credits to the new limit
|
||||
updateData.generationCreditsRemaining = limit;
|
||||
}
|
||||
|
||||
const user = await db.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
@@ -62,6 +74,8 @@ export async function PATCH(
|
||||
flagged: true,
|
||||
flaggedAt: true,
|
||||
flaggedReason: true,
|
||||
dailyGenerationLimit: true,
|
||||
generationCreditsRemaining: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,6 +102,8 @@ export async function GET(request: NextRequest) {
|
||||
flagged: true,
|
||||
flaggedAt: true,
|
||||
flaggedReason: true,
|
||||
dailyGenerationLimit: true,
|
||||
generationCreditsRemaining: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
|
||||
66
src/app/api/cron/reset-credits/route.ts
Normal file
66
src/app/api/cron/reset-credits/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Cron job endpoint to reset daily generation credits for all users.
|
||||
* Should be called daily via external cron service (e.g., Vercel Cron, GitHub Actions).
|
||||
*
|
||||
* Requires CRON_SECRET environment variable for authentication.
|
||||
*
|
||||
* Cron Notation: 0 0 * * * (Every day at midnight)
|
||||
*
|
||||
* Example cron call:
|
||||
* curl -X POST https://your-domain.com/api/cron/reset-credits \
|
||||
* -H "Authorization: Bearer YOUR_CRON_SECRET"
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
// Verify the secret
|
||||
const authHeader = request.headers.get("authorization");
|
||||
const cronSecret = process.env.CRON_SECRET;
|
||||
|
||||
if (!cronSecret) {
|
||||
console.error("CRON_SECRET is not configured");
|
||||
return NextResponse.json(
|
||||
{ error: "Cron secret not configured" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const providedSecret = authHeader?.replace("Bearer ", "");
|
||||
|
||||
if (providedSecret !== cronSecret) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Reset generation credits for all users to their daily limit
|
||||
const result = await db.$executeRaw`
|
||||
UPDATE users
|
||||
SET "generationCreditsRemaining" = "dailyGenerationLimit",
|
||||
"generationCreditsResetAt" = NOW()
|
||||
`;
|
||||
|
||||
console.log(`Daily credit reset completed. Updated ${result} users.`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Daily generation credits reset successfully",
|
||||
usersUpdated: result,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error resetting daily credits:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to reset credits" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Also support GET for simple health checks / manual triggers via browser
|
||||
export async function GET(request: NextRequest) {
|
||||
return POST(request);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
initializeMediaGenerators,
|
||||
getMediaGeneratorPlugin,
|
||||
@@ -20,10 +21,25 @@ export async function GET() {
|
||||
const imageModels = getAvailableModels("image");
|
||||
const videoModels = getAvailableModels("video");
|
||||
|
||||
// Get user's credit info
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: {
|
||||
generationCreditsRemaining: true,
|
||||
dailyGenerationLimit: true,
|
||||
flagged: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
available,
|
||||
imageModels,
|
||||
videoModels,
|
||||
credits: {
|
||||
remaining: user?.generationCreditsRemaining ?? 0,
|
||||
daily: user?.dailyGenerationLimit ?? 0,
|
||||
},
|
||||
canGenerate: !user?.flagged && (user?.generationCreditsRemaining ?? 0) > 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +51,35 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check user's credits and flagged status
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: {
|
||||
generationCreditsRemaining: true,
|
||||
flagged: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Block flagged users
|
||||
if (user.flagged) {
|
||||
return NextResponse.json(
|
||||
{ error: "Your account has been flagged. Media generation is disabled." },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check credits
|
||||
if (user.generationCreditsRemaining <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No generation credits remaining. Credits reset daily." },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { prompt, model, provider, type, inputImageUrl, resolution, aspectRatio } = body;
|
||||
|
||||
@@ -72,6 +117,16 @@ export async function POST(request: NextRequest) {
|
||||
aspectRatio,
|
||||
});
|
||||
|
||||
// Deduct one credit after successful generation start
|
||||
await db.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: {
|
||||
generationCreditsRemaining: {
|
||||
decrement: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
taskId: task.taskId,
|
||||
|
||||
@@ -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, Flag, AlertTriangle } from "lucide-react";
|
||||
import { MoreHorizontal, Shield, User, Trash2, BadgeCheck, Search, Loader2, ChevronLeft, ChevronRight, Filter, Flag, AlertTriangle, Sparkles } from "lucide-react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -54,6 +54,8 @@ interface UserData {
|
||||
flagged: boolean;
|
||||
flaggedAt: string | null;
|
||||
flaggedReason: string | null;
|
||||
dailyGenerationLimit: number;
|
||||
generationCreditsRemaining: number;
|
||||
createdAt: string;
|
||||
_count: {
|
||||
prompts: number;
|
||||
@@ -73,6 +75,8 @@ export function UsersTable() {
|
||||
const tCommon = useTranslations("common");
|
||||
const locale = useLocale();
|
||||
const [deleteUserId, setDeleteUserId] = useState<string | null>(null);
|
||||
const [editCreditsUser, setEditCreditsUser] = useState<UserData | null>(null);
|
||||
const [newCreditLimit, setNewCreditLimit] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Pagination and search state
|
||||
@@ -198,6 +202,35 @@ export function UsersTable() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditCredits = (user: UserData) => {
|
||||
setEditCreditsUser(user);
|
||||
setNewCreditLimit(user.dailyGenerationLimit.toString());
|
||||
};
|
||||
|
||||
const handleSaveCredits = async () => {
|
||||
if (!editCreditsUser) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${editCreditsUser.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dailyGenerationLimit: newCreditLimit }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to update credits");
|
||||
|
||||
toast.success(t("creditsUpdated"));
|
||||
fetchUsers(currentPage, searchQuery, userFilter);
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast.error(t("creditsUpdateFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setEditCreditsUser(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
@@ -295,6 +328,10 @@ export function UsersTable() {
|
||||
<Flag className="h-4 w-4 mr-2" />
|
||||
{user.flagged ? t("unflag") : t("flag")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEditCredits(user)}>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{t("editCredits")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
@@ -393,6 +430,10 @@ export function UsersTable() {
|
||||
<Flag className="h-4 w-4 mr-2" />
|
||||
{user.flagged ? t("unflag") : t("flag")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEditCredits(user)}>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{t("editCredits")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
@@ -466,6 +507,43 @@ export function UsersTable() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Edit Credits Dialog */}
|
||||
<AlertDialog open={!!editCreditsUser} onOpenChange={() => setEditCreditsUser(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("editCreditsTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("editCreditsDescription", { username: editCreditsUser?.username || "" })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">{t("dailyLimit")}</label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
value={newCreditLimit}
|
||||
onChange={(e) => setNewCreditLimit(e.target.value)}
|
||||
placeholder="10"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("currentCredits", {
|
||||
remaining: editCreditsUser?.generationCreditsRemaining ?? 0,
|
||||
limit: editCreditsUser?.dailyGenerationLimit ?? 0
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSaveCredits} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
|
||||
{t("save")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user