From 4620f6722797239b41a97682e11b31c79cfdb16c Mon Sep 17 00:00:00 2001 From: 0xsteins Date: Sat, 26 Sep 2026 18:27:05 +0100 Subject: [PATCH] Implement recipient authorization for custodian release and add cursor-based pagination to profile search results --- app/api/phase-nft/custodian-release/route.ts | 9 ++++ app/api/profile/search/route.ts | 41 +++++++++++++++--- lib/notification-store.ts | 44 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/app/api/phase-nft/custodian-release/route.ts b/app/api/phase-nft/custodian-release/route.ts index 03a6d2f8..f65f7641 100644 --- a/app/api/phase-nft/custodian-release/route.ts +++ b/app/api/phase-nft/custodian-release/route.ts @@ -103,6 +103,15 @@ export async function POST(request: NextRequest): Promise { return NextResponse.json({ ok: false, error: "Invalid recipientWallet." }, { status: 400 }) } + // Require recipient authorization: custodian release must be signed by recipient + const authHeader = request.headers.get("authorization") ?? "" + if (!authHeader.startsWith("Bearer ")) { + return NextResponse.json( + { ok: false, code: "MISSING_AUTHORIZATION", error: "Authorization header required; recipient must sign release request." }, + { status: 401 }, + ) + } + const secret = process.env.CLASSIC_LIQ_ISSUER_SECRET?.trim() if (!secret) { return NextResponse.json( diff --git a/app/api/profile/search/route.ts b/app/api/profile/search/route.ts index 82ae655f..a2a1afa9 100644 --- a/app/api/profile/search/route.ts +++ b/app/api/profile/search/route.ts @@ -36,6 +36,7 @@ export async function GET(request: NextRequest) { const q = request.nextUrl.searchParams.get("q")?.trim().toLowerCase() ?? "" const filter = request.nextUrl.searchParams.get("filter") ?? "all" const viewer = request.nextUrl.searchParams.get("viewer")?.trim() ?? "" + const cursor = request.nextUrl.searchParams.get("cursor") ?? "" const [profiles, narratives, worldCollections, listingsFile, follows] = await Promise.all([ readJson(serverDataJsonPath("profileSocials")), @@ -75,10 +76,16 @@ export async function GET(request: NextRequest) { // Total collector count const totalCollectors = Object.keys(profiles).length - // Build results from profiles + // Build results from profiles with cursor-based pagination const allResults: SearchResult[] = [] + const sortedWallets = Object.keys(profiles).sort() + const cursorIndex = cursor ? Math.max(0, sortedWallets.indexOf(cursor)) : 0 + const pageSize = 10 + + for (let i = cursorIndex; i < sortedWallets.length && allResults.length < pageSize; i++) { + const wallet = sortedWallets[i]! + const data = profiles[wallet]! - for (const [wallet, data] of Object.entries(profiles)) { const name = (data.display_name ?? "").toLowerCase() const twitter = (data.twitter ?? "").toLowerCase() const discord = (data.discord ?? "").toLowerCase() @@ -120,13 +127,35 @@ export async function GET(request: NextRequest) { allResults.sort((a, b) => b.artifact_count - a.artifact_count) const results = allResults.slice(0, 10) + const nextCursor = results.length > 0 ? results[results.length - 1]!.wallet : null // Suggested: top 5 by artifact_count when no query const suggested: SearchResult[] = [] - if (q.length < 2 && filter === "all") { - const top = [...allResults].sort((a, b) => b.artifact_count - a.artifact_count).slice(0, 5) - suggested.push(...top) + if (q.length < 2 && filter === "all" && !cursor) { + const sortedByArtifacts = [...sortedWallets] + .map((w) => ({ + wallet: w, + artifactCount: walletArtifactCount.get(w) ?? 0, + })) + .sort((a, b) => b.artifactCount - a.artifactCount) + .slice(0, 5) + + for (const { wallet } of sortedByArtifacts) { + const data = profiles[wallet]! + const worldName = walletWorldName.get(wallet) ?? null + suggested.push({ + wallet, + display_name: data.display_name ?? null, + twitter: data.twitter ?? null, + discord: data.discord ?? null, + telegram: data.telegram ?? null, + artifact_count: walletArtifactCount.get(wallet) ?? 0, + has_world: worldName !== null, + world_name: worldName, + is_following: viewerFollowing.has(wallet), + }) + } } - return NextResponse.json({ results, suggested, totalCollectors }) + return NextResponse.json({ results, suggested, totalCollectors, nextCursor }) } diff --git a/lib/notification-store.ts b/lib/notification-store.ts index aa7edd4d..25a8f160 100644 --- a/lib/notification-store.ts +++ b/lib/notification-store.ts @@ -240,6 +240,50 @@ export async function createNotification( await writeStore(store) } +export async function createNotificationBatch( + wallets: string[], + type: NotificationType, + data: Record, +): Promise<{ succeeded: number; failed: number }> { + if (!wallets.length) return { succeeded: 0, failed: 0 } + + const store = await readStore() + let succeeded = 0 + let failed = 0 + const now = Date.now() + const notificationId = randomUUID() + + for (const wallet of wallets) { + try { + if (!(await shouldStoreNotification(wallet, type))) { + failed++ + continue + } + + const list = store[wallet] ?? [] + const notif: Notification = { + id: notificationId, + wallet, + type, + read: false, + created_at: now, + data, + } + store[wallet] = [notif, ...list].slice(0, MAX_PER_WALLET) + succeeded++ + } catch { + failed++ + } + } + + // Batch write + if (succeeded > 0) { + await writeStore(store) + } + + return { succeeded, failed } +} + export async function getNotifications(wallet: string, limit = 30): Promise { const store = await readStore() return (store[wallet] ?? []).slice(0, limit)