diff --git a/.gitignore b/.gitignore index 63231da..e308e68 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist/ build/ /app/generated/prisma docker-compose.yml +deploy.sh diff --git a/app/api/tiktok/callback/route.ts b/app/api/tiktok/callback/route.ts index 741c194..5aeda40 100644 --- a/app/api/tiktok/callback/route.ts +++ b/app/api/tiktok/callback/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server"; import { exchangeCodeForTokens } from "@/lib/tiktok"; import { prisma } from "@/lib/prisma"; +const baseUrl = process.env.NEXTAUTH_URL || "https://marouette.fun"; + export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get("code"); @@ -9,14 +11,13 @@ export async function GET(request: NextRequest) { const error = searchParams.get("error"); if (error || !code || !state) { - return NextResponse.redirect(new URL("/tiktok?error=access_denied", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=access_denied`); } try { - // userId stocké dans le record PKCE lors du /connect (pas besoin de session) const pkceRecord = await prisma.tikTokPKCE.findUnique({ where: { state } }); if (!pkceRecord) { - return NextResponse.redirect(new URL("/tiktok?error=missing_verifier", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=missing_verifier`); } await prisma.tikTokPKCE.delete({ where: { state } }); @@ -29,23 +30,48 @@ export async function GET(request: NextRequest) { const tokens = await exchangeCodeForTokens(code, codeVerifier); const expiresAt = new Date(Date.now() + tokens.expires_in * 1000); + // Upsert sur userId — plusieurs users peuvent partager le même compte TikTok await prisma.tikTokToken.upsert({ where: { userId }, - create: { userId, openId: tokens.open_id, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt }, - update: { openId: tokens.open_id, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt }, + update: { + openId: tokens.open_id, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt, + }, + create: { + userId, + openId: tokens.open_id, + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt, + }, }); - const existing = await prisma.trackedAccount.findFirst({ where: { userId, platform: "tiktok" } }); - if (!existing) { + // TrackedAccount : update si existe, sinon create + const existing = await prisma.trackedAccount.findFirst({ + where: { userId, platform: "tiktok" }, + }); + + if (existing) { + await prisma.trackedAccount.update({ + where: { id: existing.id }, + data: { username: tokens.open_id, accountId: tokens.open_id }, + }); + } else { await prisma.trackedAccount.create({ - data: { userId, platform: "tiktok", username: tokens.open_id, accountId: tokens.open_id }, + data: { + userId, + platform: "tiktok", + username: tokens.open_id, + accountId: tokens.open_id, + }, }); } - return NextResponse.redirect(new URL("/tiktok?connected=1", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?connected=1`); } catch (err) { console.error("[TikTok callback error]", err); - return NextResponse.redirect(new URL("/tiktok?error=token_exchange", request.url)); + return NextResponse.redirect(`${baseUrl}/tiktok?error=token_exchange`); } -} - +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4b7bc34..27957d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - "3001:3001" environment: - NODE_ENV=production + - AUTH_URL=${AUTH_URL} - DATABASE_URL=${DATABASE_URL} - NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} diff --git a/lib/tiktok.ts b/lib/tiktok.ts index c0a9750..4005d88 100644 --- a/lib/tiktok.ts +++ b/lib/tiktok.ts @@ -43,6 +43,10 @@ export function getTikTokAuthUrl(state: string, codeChallenge: string): string { state, code_challenge: codeChallenge, code_challenge_method: "S256", + // "select_account" seul est ignoré par TikTok si une session est active. + // On passe les deux pour forcer l'affichage du sélecteur de compte à chaque fois. + prompt: "select_account", + force_login: "true", }); return `${TIKTOK_AUTH_URL}?${params.toString()}`; } @@ -142,9 +146,4 @@ export async function fetchUserStats(accessToken: string, openId: string): Promi displayName: user.display_name ?? "", avatarUrl: user.avatar_url ?? "", }; -} - - - - - +} \ No newline at end of file diff --git a/prisma/migrations/20260311154019_remove_openid_unique/migration.sql b/prisma/migrations/20260311154019_remove_openid_unique/migration.sql new file mode 100644 index 0000000..13c2f32 --- /dev/null +++ b/prisma/migrations/20260311154019_remove_openid_unique/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "TikTokToken_openId_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c1ef6b3..1b16f84 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,7 +23,7 @@ model TikTokToken { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) - openId String @unique + openId String accessToken String refreshToken String expiresAt DateTime