Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions app/api/creator/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/lib/dbConnect';
import Story from '../../../../models/Story';
import { UserInteraction } from '../../../../models/UserInteraction';
import { VALIDATION_PATTERNS } from '@/lib/constants';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const wallet = (searchParams.get('wallet') || '').trim().toLowerCase();

if (!wallet || !VALIDATION_PATTERNS.walletAddress.test(wallet)) {
return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 });
}

await dbConnect();

const stories = await Story.find({ authorWallet: wallet })
.select({ _id: 1, title: 1, status: 1, updatedAt: 1, nftTokenId: 1, nftTxHash: 1 })
.sort({ updatedAt: -1 })
.lean();
Comment on lines +18 to +21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound this route to avoid unbounded query and aggregation load.

The current flow fetches all stories and aggregates over all IDs in one request. For large wallets, this can degrade latency and memory usage significantly. Add an enforced limit (and ideally pagination/cursor support).

💡 Proposed hard-cap fix
 export async function GET(request: NextRequest) {
   try {
     const { searchParams } = new URL(request.url);
     const wallet = (searchParams.get('wallet') || '').trim().toLowerCase();
+    const limit = Math.min(
+      Math.max(Number(searchParams.get('limit') || 200), 1),
+      500
+    );

@@
     const stories = await Story.find({ authorWallet: wallet })
       .select({ _id: 1, title: 1, status: 1, updatedAt: 1, nftTokenId: 1, nftTxHash: 1 })
       .sort({ updatedAt: -1 })
+      .limit(limit)
       .lean();

Also applies to: 30-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/creator/analytics/route.ts` around lines 18 - 21, The route currently
calls Story.find(...) and loads all matching stories into stories, causing
unbounded memory/CPU for large wallets; modify the handler that calls Story.find
to enforce a hard cap (e.g., limit parameter) and add pagination/cursor support
by accepting limit and cursor/query params, applying .limit(limit) and a cursor
filter (e.g., updatedAt/_id > cursor) to the query, and return the next-cursor
along with the page of stories so downstream aggregation uses only that page;
update the code paths that aggregate over the returned story IDs to operate on
the paged slice rather than the entire set (refer to the
Story.find(...).select(...).sort(...) usage and any aggregation code that
consumes stories).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bibhupradhanofficial Bound the route

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


if (stories.length === 0) {
return NextResponse.json({
totals: { views: 0, likes: 0, bookmarks: 0, mints: 0 },
stories: [],
});
}

const storyIds = stories.map((story: any) => story._id);

const aggregation = await UserInteraction.aggregate([
{
$match: {
storyId: { $in: storyIds },
type: { $in: ['VIEW', 'LIKE', 'BOOKMARK'] },
},
},
{
$group: {
_id: { storyId: '$storyId', type: '$type' },
value: { $sum: '$value' },
},
},
]);

const metricsByStoryId = new Map<
string,
{ views: number; likes: number; bookmarks: number }
>();

for (const row of aggregation) {
const storyId = row?._id?.storyId?.toString?.() || '';
const type = row?._id?.type as 'VIEW' | 'LIKE' | 'BOOKMARK';
const value = Number(row?.value) || 0;
if (!storyId || !type) {
continue;
}
const existing = metricsByStoryId.get(storyId) || { views: 0, likes: 0, bookmarks: 0 };
if (type === 'VIEW') existing.views += value;
if (type === 'LIKE') existing.likes += value;
if (type === 'BOOKMARK') existing.bookmarks += value;
metricsByStoryId.set(storyId, existing);
}

const storyRows = stories.map((story: any) => {
const storyId = story._id.toString();
const engagement = metricsByStoryId.get(storyId) || { views: 0, likes: 0, bookmarks: 0 };
const minted = story.nftTokenId ? 1 : 0;
return {
storyId,
title: story.title || 'Untitled',
status: story.status || 'draft',
updatedAt: story.updatedAt ? new Date(story.updatedAt).getTime() : Date.now(),
views: engagement.views,
likes: engagement.likes,
bookmarks: engagement.bookmarks,
mints: minted,
};
});

const totals = storyRows.reduce(
(acc, row) => {
acc.views += row.views;
acc.likes += row.likes;
acc.bookmarks += row.bookmarks;
acc.mints += row.mints;
return acc;
},
{ views: 0, likes: 0, bookmarks: 0, mints: 0 }
);

return NextResponse.json({
totals,
stories: storyRows,
});
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

73 changes: 73 additions & 0 deletions app/api/creator/drafts/[draftKey]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/lib/dbConnect';
import Draft from '../../../../../models/Draft';
import { VALIDATION_PATTERNS } from '@/lib/constants';

export async function GET(
request: NextRequest,
{ params }: { params: { draftKey: string } }
) {
try {
const { searchParams } = new URL(request.url);
const wallet = (searchParams.get('wallet') || '').trim().toLowerCase();
const draftKey = (params.draftKey || '').trim();

if (!wallet || !VALIDATION_PATTERNS.walletAddress.test(wallet)) {
return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 });
}
if (!draftKey) {
return NextResponse.json({ error: 'draftKey is required' }, { status: 400 });
}

await dbConnect();

const draft = await Draft.findOne({ draftKey, ownerWallet: wallet }).lean();
if (!draft) {
return NextResponse.json({ error: 'Draft not found' }, { status: 404 });
}

const currentUpdatedAt = draft.current?.updatedAt
? new Date(draft.current.updatedAt).getTime()
: Date.now();
Comment thread
bibhupradhanofficial marked this conversation as resolved.

return NextResponse.json({
draftKey: draft.draftKey,
storyType: draft.storyType,
storyFormat: draft.storyFormat,
ownerWallet: draft.ownerWallet,
ownerRole: draft.ownerRole,
current: {
title: draft.current?.title || '',
description: draft.current?.description || '',
genre: draft.current?.genre || '',
content: draft.current?.content || '',
coverImageName: draft.current?.coverImageName || '',
updatedAt: currentUpdatedAt,
version: draft.current?.version || 1,
},
versions: (draft.versions || []).map((version: any) => ({
id: version._id?.toString?.() || '',
title: version.title || '',
description: version.description || '',
genre: version.genre || '',
content: version.content || '',
coverImageName: version.coverImageName || '',
updatedAt: version.updatedAt ? new Date(version.updatedAt).getTime() : Date.now(),
version: version.version || 1,
reason: version.reason || 'autosave',
})),
aiMetadata: {
pipelineState: draft.aiMetadata?.pipelineState || 'idle',
suggestedEdits: draft.aiMetadata?.suggestedEdits || [],
lastEditedByAIAt: draft.aiMetadata?.lastEditedByAIAt
? new Date(draft.aiMetadata.lastEditedByAIAt).getTime()
: null,
},
createdAt: draft.createdAt ? new Date(draft.createdAt).getTime() : null,
updatedAt: draft.updatedAt ? new Date(draft.updatedAt).getTime() : null,
});
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

92 changes: 92 additions & 0 deletions app/api/creator/drafts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/lib/dbConnect';
import Draft from '../../../../models/Draft';
import { VALIDATION_PATTERNS } from '@/lib/constants';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const wallet = (searchParams.get('wallet') || '').trim().toLowerCase();
const query = (searchParams.get('q') || '').trim();
const pipelineState = (searchParams.get('pipeline') || '').trim();
const page = Math.max(1, Number.parseInt(searchParams.get('page') || '1', 10) || 1);
const limit = Math.max(
1,
Math.min(50, Number.parseInt(searchParams.get('limit') || '20', 10) || 20)
);

if (!wallet || !VALIDATION_PATTERNS.walletAddress.test(wallet)) {
return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 });
}
Comment on lines +18 to +20
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking for middleware files ==="
fd 'middleware.ts' --type f

echo
echo "=== Listing creator API route files ==="
fd 'route.ts' 'app/api/creator' --type f

echo
echo "=== Searching for auth/session verification in creator APIs ==="
rg -n --type=ts -C3 'getServerSession|auth\(|verify|jwt|clerk|siwe|signature|session' 'app/api/creator' 2>/dev/null || echo "No auth patterns found"

echo
echo "=== Checking wallet-param based access in draft routes ==="
rg -n --type=ts -C3 'searchParams\.get.*wallet|ownerWallet|walletAddress' 'app/api/creator/drafts' 2>/dev/null || echo "Pattern not found, checking entire file"

echo
echo "=== Full content of drafts/route.ts to understand auth context ==="
wc -l app/api/creator/drafts/route.ts && head -50 app/api/creator/drafts/route.ts

Repository: IndieHub25/GroqTales

Length of output: 6903


Implement authentication and caller-ownership verification for draft access.

The GET endpoints at app/api/creator/drafts/route.ts (lines 18-20, 24-25) and app/api/creator/drafts/[draftKey]/route.ts (lines 12, 15-17, 24) accept a wallet address as a query parameter and retrieve drafts with matching ownerWallet, but provide no authentication or ownership verification. This allows any caller to access another creator's drafts by passing a valid wallet address.

Instead of filtering by client-supplied wallet, these endpoints must:

  1. Authenticate the caller (via session, JWT, or other mechanism)
  2. Verify the caller owns the wallet they're querying, or reject access
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/creator/drafts/route.ts` around lines 18 - 20, The GET handlers in
app/api/creator/drafts/route.ts and app/api/creator/drafts/[draftKey]/route.ts
currently trust a client-supplied wallet query and filter by ownerWallet; change
them to authenticate the caller (via your existing session/JWT middleware or
auth util) inside the GET handler, derive the caller's wallet from the
authenticated session, and reject the request if no authenticated wallet is
present; then use that authenticated wallet to query drafts (instead of the
query param) and for single-draft access verify the draft.ownerWallet ===
authenticatedWallet (or return 403) when resolving the draft by draftKey; remove
or ignore the client-supplied wallet query and add clear 401/403 responses for
unauthenticated/unauthorized callers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bibhupradhanofficial implement authentication with already set up supabase setup

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


await dbConnect();

const filter: Record<string, any> = { ownerWallet: wallet };

if (pipelineState && ['idle', 'ready', 'processing'].includes(pipelineState)) {
filter['aiMetadata.pipelineState'] = pipelineState;
}

if (query) {
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
filter.$or = [
{ draftKey: { $regex: escaped, $options: 'i' } },
{ 'current.title': { $regex: escaped, $options: 'i' } },
{ 'current.genre': { $regex: escaped, $options: 'i' } },
{ storyType: { $regex: escaped, $options: 'i' } },
{ storyFormat: { $regex: escaped, $options: 'i' } },
];
}

const total = await Draft.countDocuments(filter);
const items = await Draft.find(filter)
.sort({ updatedAt: -1 })
.skip((page - 1) * limit)
.limit(limit)
.select({
draftKey: 1,
storyType: 1,
storyFormat: 1,
ownerWallet: 1,
ownerRole: 1,
current: { title: 1, genre: 1, updatedAt: 1, version: 1 },
aiMetadata: { pipelineState: 1, lastEditedByAIAt: 1 },
createdAt: 1,
updatedAt: 1,
})
.lean();

return NextResponse.json({
items: items.map((item: any) => ({
draftKey: item.draftKey,
storyType: item.storyType,
storyFormat: item.storyFormat,
ownerWallet: item.ownerWallet,
ownerRole: item.ownerRole,
current: {
title: item.current?.title || '',
genre: item.current?.genre || '',
updatedAt: item.current?.updatedAt ? new Date(item.current.updatedAt).getTime() : null,
version: item.current?.version || 1,
},
aiMetadata: {
pipelineState: item.aiMetadata?.pipelineState || 'idle',
lastEditedByAIAt: item.aiMetadata?.lastEditedByAIAt
? new Date(item.aiMetadata.lastEditedByAIAt).getTime()
: null,
},
createdAt: item.createdAt ? new Date(item.createdAt).getTime() : null,
updatedAt: item.updatedAt ? new Date(item.updatedAt).getTime() : null,
})),
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
Comment thread
Drago-03 marked this conversation as resolved.
});
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

86 changes: 86 additions & 0 deletions app/api/creator/nfts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/lib/dbConnect';
import Story from '../../../../models/Story';
import { BLOCKCHAIN_CONFIG, VALIDATION_PATTERNS } from '@/lib/constants';
import { checkTxStatus } from '@/lib/blockchain';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const wallet = (searchParams.get('wallet') || '').trim().toLowerCase();

if (!wallet || !VALIDATION_PATTERNS.walletAddress.test(wallet)) {
return NextResponse.json({ error: 'Invalid wallet address' }, { status: 400 });
}

await dbConnect();

const stories = await Story.find({ authorWallet: wallet })
.select({
_id: 1,
title: 1,
status: 1,
ipfsHash: 1,
nftTxHash: 1,
nftTokenId: 1,
updatedAt: 1,
})
.sort({ updatedAt: -1 })
.lean();

const explorerBase = BLOCKCHAIN_CONFIG.networks.monad.explorerUrl;
const contract = BLOCKCHAIN_CONFIG.contracts.storyNFT || '';

const candidates = stories.filter((story: any) => story.status !== 'draft');

const payload = await Promise.all(candidates.map(async (story: any) => {
const txHash = story.nftTxHash || undefined;
let tokenId = story.nftTokenId || undefined;
const status = story.status || 'draft';
let syncStatus: 'missing' | 'pending' | 'confirmed' | 'failed' =
status === 'failed'
? 'failed'
: tokenId
? 'confirmed'
: txHash || status === 'publishing'
? 'pending'
: 'missing';

if (txHash && syncStatus === 'pending') {
try {
const chainStatus = await checkTxStatus(txHash);
if (chainStatus.status === 'confirmed') {
syncStatus = 'confirmed';
tokenId = tokenId || (chainStatus as any).tokenId || undefined;
} else if (chainStatus.status === 'reverted') {
syncStatus = 'failed';
}
} catch {
}
}
Comment on lines +36 to +60
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard on-chain checks with bounded execution and timeout.

This endpoint can fire one blockchain status request per story in parallel, with no timeout. Under larger wallets, this can overload RPC and tie up request handling. Also, silent catch hides operational failures.

🛡️ Safer pattern (timeout + controlled execution)
-    const payload = await Promise.all(candidates.map(async (story: any) => {
+    const withTimeout = <T,>(p: Promise<T>, ms: number) =>
+      Promise.race([
+        p,
+        new Promise<never>((_, reject) =>
+          setTimeout(() => reject(new Error('tx-status-timeout')), ms)
+        ),
+      ]);
+
+    const payload = [];
+    for (const story of candidates) {
@@
-        if (txHash && syncStatus === 'pending') {
+        if (txHash && syncStatus === 'pending') {
           try {
-            const chainStatus = await checkTxStatus(txHash);
+            const chainStatus = await withTimeout(checkTxStatus(txHash), 5000);
             if (chainStatus.status === 'confirmed') {
               syncStatus = 'confirmed';
               tokenId = tokenId || (chainStatus as any).tokenId || undefined;
             } else if (chainStatus.status === 'reverted') {
               syncStatus = 'failed';
             }
-          } catch {
+          } catch (error) {
+            console.warn(`[creator/nfts] tx status check failed for ${txHash}:`, error);
           }
         }
@@
-      }));
+      payload.push({
+        storyId: story._id.toString(),
+        title: story.title || 'Untitled',
+        status,
+        ipfsHash: story.ipfsHash || undefined,
+        nftTxHash: txHash,
+        nftTokenId: tokenId,
+        syncStatus,
+        explorer,
+        updatedAt: story.updatedAt ? new Date(story.updatedAt).getTime() : Date.now(),
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/creator/nfts/route.ts` around lines 36 - 60, The on-chain check loop
inside the candidates.map (where txHash, tokenId, syncStatus are computed and
checkTxStatus is called) must be made bounded and fail-fast: wrap each
checkTxStatus(txHash) call with a timeout (e.g., reject after a few seconds) and
run the checks with a concurrency limiter or in small batches (instead of firing
unbounded Promise.all across all stories), and replace the empty catch with
logging/propagating the error so operational failures are visible; adjust logic
to treat timed-out/rejected checks as 'pending' or 'missing' as appropriate and
still allow tokenId to be updated when a successful response returns.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


const explorer = {
tx: txHash ? `${explorerBase}/tx/${txHash}` : undefined,
token: contract && tokenId ? `${explorerBase}/token/${contract}?a=${tokenId}` : undefined,
contract: contract ? `${explorerBase}/address/${contract}` : undefined,
};

return {
storyId: story._id.toString(),
title: story.title || 'Untitled',
status,
ipfsHash: story.ipfsHash || undefined,
nftTxHash: txHash,
nftTokenId: tokenId,
syncStatus,
explorer,
updatedAt: story.updatedAt ? new Date(story.updatedAt).getTime() : Date.now(),
};
}));

return NextResponse.json(payload);
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}

Loading
Loading