feat (Worker): add a worker to check and adjust the TikTok graph

This commit is contained in:
Pierre Ryssen
2026-03-13 12:00:00 +01:00
parent 61f426ef3c
commit 4ef8913759
11 changed files with 960 additions and 52 deletions

View File

@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
const PLAN_RANK: Record<string, number> = { free: 0, pro: 1, elite: 2, team: 3 };
const PERIOD_MIN_PLAN: Record<string, string> = {
"7d": "free",
"30d": "pro",
"90d": "pro",
"all": "elite",
};
const PERIOD_DAYS: Record<string, number | null> = {
"7d": 7,
"30d": 30,
"90d": 90,
"all": null,
};
// Plan max history cap (even for "all")
const PLAN_MAX_DAYS: Record<string, number | null> = {
free: 7,
pro: 90,
elite: null, // illimité
team: null,
};
async function getAuthedUserId() {
const session = await auth();
if (!session?.user) return null;
return (session.user as { id?: string }).id ?? null;
}
export async function GET(req: NextRequest) {
const userId = await getAuthedUserId();
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const period = req.nextUrl.searchParams.get("period") ?? "7d";
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
const plan = (user as any).plan ?? "free";
// Vérif accès à la période demandée
const requiredPlan = PERIOD_MIN_PLAN[period] ?? "free";
if (PLAN_RANK[plan] < PLAN_RANK[requiredPlan]) {
return NextResponse.json({ error: "Plan insuffisant pour cette période" }, { status: 403 });
}
const account = await prisma.trackedAccount.findFirst({
where: { userId, platform: "tiktok" },
});
if (!account) return NextResponse.json([], { status: 200 });
// Calcul de la date de début
const periodDays = PERIOD_DAYS[period];
const planMaxDays = PLAN_MAX_DAYS[plan];
let since: Date | undefined;
if (periodDays !== null) {
since = new Date(Date.now() - periodDays * 86_400_000);
} else if (planMaxDays !== null) {
since = new Date(Date.now() - planMaxDays * 86_400_000);
}
// sinon : illimité (team/elite + all)
const snapshots = await prisma.snapshot.findMany({
where: {
accountId: account.id,
...(since ? { createdAt: { gte: since } } : {}),
},
orderBy: { createdAt: "asc" },
select: {
createdAt: true,
followers: true,
likes: true,
videoCount: true,
},
});
return NextResponse.json(snapshots);
}
export async function POST(req: NextRequest) {
const userId = await getAuthedUserId();
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const { followers, likes, videoCount, displayName, openId } = body;
if (followers === undefined) {
return NextResponse.json({ error: "Champ 'followers' requis" }, { status: 400 });
}
// Upsert TrackedAccount
let account = await prisma.trackedAccount.findFirst({
where: { userId, platform: "tiktok" },
});
if (!account) {
account = await prisma.trackedAccount.create({
data: {
userId,
platform: "tiktok",
username: displayName ?? openId ?? "unknown",
accountId: openId ?? userId,
},
});
}
const snapshot = await prisma.snapshot.create({
data: {
accountId: account.id,
followers: followers ?? 0,
likes: likes ?? 0,
videoCount: videoCount ?? 0,
views: 0,
},
});
return NextResponse.json(snapshot, { status: 201 });
}

View File

@@ -16,12 +16,14 @@ export async function GET() {
}
if (process.env.NODE_ENV === "development") {
const user = await prisma.user.findUnique({ where: { id: userId } });
return NextResponse.json({
followers: 124,
likes: 856,
videoCount: 1,
displayName: "CrowMate studio",
avatarUrl: "",
plan: (user as any)?.plan ?? "free",
});
}
@@ -36,20 +38,16 @@ export async function GET() {
if (expiresAt.getTime() - Date.now() < 60_000) {
try {
const refreshed = await refreshAccessToken(refreshToken);
const newExpiresAt = new Date(Date.now() + refreshed.expires_in * 1000);
await prisma.tikTokToken.update({
where: { userId },
data: {
accessToken: refreshed.access_token,
refreshToken: refreshed.refresh_token,
expiresAt: newExpiresAt,
expiresAt: new Date(Date.now() + refreshed.expires_in * 1000),
},
});
accessToken = refreshed.access_token;
refreshToken = refreshed.refresh_token;
expiresAt = newExpiresAt;
} catch (err) {
console.error("[TikTok stats refresh error]", err);
return NextResponse.json({ error: "Token refresh failed" }, { status: 401 });
@@ -58,7 +56,38 @@ export async function GET() {
try {
const stats = await fetchUserStats(accessToken, openId);
return NextResponse.json(stats);
// Upsert TrackedAccount + snapshot automatique
try {
let account = await prisma.trackedAccount.findFirst({
where: { userId, platform: "tiktok" },
});
if (!account) {
account = await prisma.trackedAccount.create({
data: {
userId,
platform: "tiktok",
username: stats.displayName ?? openId,
accountId: openId,
},
});
}
await prisma.snapshot.create({
data: {
accountId: account.id,
followers: stats.followers ?? 0,
likes: stats.likes ?? 0,
videoCount: stats.videoCount ?? 0,
views: 0,
},
});
} catch (snapshotErr) {
console.error("[TikTok snapshot save error]", snapshotErr);
}
// Inclure le plan dans la réponse
const user = await prisma.user.findUnique({ where: { id: userId } });
return NextResponse.json({ ...stats, plan: (user as any)?.plan ?? "free" });
} catch (err) {
console.error("[TikTok stats fetch error]", err);
return NextResponse.json({ error: "Failed to fetch stats" }, { status: 500 });