Harden milestone sharing and X publishing

This commit is contained in:
2026-08-14 18:19:55 +02:00
parent d8f7202bf6
commit 4ec70ed30f
8 changed files with 191 additions and 63 deletions

View File

@@ -1,7 +1,10 @@
import { randomBytes } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getWwwOrigin } from '@/lib/hosts';
import { getSessionUserId } from '@/lib/session';
import { buildMilestoneCardSnapshot, buildMilestonePostForQr, milestoneThreshold, socialLocale } from '@/lib/social-milestones';
import { buildMilestonePostForQr, milestoneThreshold, socialLocale } from '@/lib/social-milestones';
import { ensureSocialMilestoneCard } from '@/lib/social-milestones-server';
export const dynamic = 'force-dynamic';
@@ -16,9 +19,15 @@ export async function GET(request: NextRequest) {
if (!user || user.socialPromptOptOut) return NextResponse.json({ milestone: null });
const milestone = await db.socialMilestone.findFirst({
where: { userId, status: { in: ['detected', 'shown'] } },
where: {
userId,
OR: [
{ status: { in: ['detected', 'shown'] } },
{ status: 'approved', brandStatus: { in: ['approved', 'processing', 'failed'] } },
],
},
orderBy: { detectedAt: 'asc' },
include: { qr: { select: { id: true, title: true } } },
include: { qr: { select: { id: true, title: true, createdAt: true } } },
});
if (!milestone) return NextResponse.json({ milestone: null });
@@ -28,20 +37,22 @@ export async function GET(request: NextRequest) {
const threshold = milestoneThreshold(milestone.kind);
if (!threshold) return NextResponse.json({ milestone: null });
const locale = socialLocale(request.nextUrl.searchParams.get('locale'));
const allScanCount = await db.qRScan.count({ where: { qrId: milestone.qr.id } });
const storedCard = milestone.cardData as Record<string, unknown> | null;
const card = storedCard
? { ...storedCard, totalScans: typeof storedCard.totalScans === 'number' ? storedCard.totalScans : allScanCount }
: buildMilestoneCardSnapshot({
primaryUseCase: user.primaryUseCase,
qrTitle: milestone.qr.title,
totalScans: allScanCount,
totalUniqueScans: threshold,
milestoneThreshold: threshold,
reachedAt: milestone.detectedAt,
trend: null,
locale,
});
const shareToken = milestone.shareToken || randomBytes(9).toString('base64url');
if (!milestone.shareToken) {
await db.socialMilestone.update({ where: { id: milestone.id }, data: { shareToken } });
}
const shareUrl = `${getWwwOrigin()}/s/m/${shareToken}`;
const card = await ensureSocialMilestoneCard({
milestoneId: milestone.id,
cardData: milestone.cardData,
kind: milestone.kind,
detectedAt: milestone.detectedAt,
language: locale,
qr: milestone.qr,
primaryUseCase: user.primaryUseCase,
refresh: ['detected', 'shown'].includes(milestone.status),
snapshotAt: new Date(),
});
return NextResponse.json({
milestone: {
@@ -51,9 +62,10 @@ export async function GET(request: NextRequest) {
brandPostUrl: milestone.brandPostUrl,
brandPostError: milestone.brandPostError,
language: locale,
shareUrl,
preview: buildMilestonePostForQr(
user.primaryUseCase,
((milestone.cardData as { totalUniqueScans?: number } | null)?.totalUniqueScans || threshold),
card.totalUniqueScans || threshold,
null,
locale,
milestone.qr.title,