/** * Verify recovered user in live novya. Issues a NEW session (does not copy old ones). * Does not print tokens, hashes, or message content. */ import { PrismaClient } from '@prisma/client'; import { generateSessionToken, hashSessionToken } from '../src/modules/auth/sessionToken.ts'; const DEST_URL = 'mysql://novya:novya@127.0.0.1:3306/novya'; const SOURCE_URL = 'mysql://novya:novya@127.0.0.1:3306/novya_test'; const TARGET_EMAIL = 'ahmadreza6137@gmail.com'; const API = 'http://localhost:3001'; async function requestJson(path: string, options: RequestInit = {}) { const response = await fetch(`${API}${path}`, options); const raw = await response.text(); let body: unknown = null; try { body = raw ? JSON.parse(raw) : null; } catch { body = raw; } return { status: response.status, body }; } function dataOf(body: unknown): Record | null { if (!body || typeof body !== 'object') return null; const record = body as Record; if (record.data && typeof record.data === 'object') { return record.data as Record; } return record; } async function main() { const dest = new PrismaClient({ datasources: { db: { url: DEST_URL } }, log: ['error'] }); const source = new PrismaClient({ datasources: { db: { url: SOURCE_URL } }, log: ['error'] }); try { const destIdentity = await dest.authIdentity.findFirst({ where: { provider: 'password', email: TARGET_EMAIL }, include: { user: { include: { profile: true, journey: true } } }, }); const sourceIdentity = await source.authIdentity.findFirst({ where: { provider: 'password', email: TARGET_EMAIL }, }); if (!destIdentity || !sourceIdentity) { throw new Error('Recovered identity missing'); } const hashMatch = destIdentity.passwordHash === sourceIdentity.passwordHash; const chatSessions = await dest.chatSession.findMany({ where: { userId: destIdentity.userId }, orderBy: { startedAt: 'asc' }, select: { id: true, sessionOrder: true, _count: { select: { messages: true } } }, }); const progress = await dest.sessionProgress.findMany({ where: { userId: destIdentity.userId }, select: { sessionOrder: true, status: true }, }); const attempts = await dest.assessmentAttempt.count({ where: { userId: destIdentity.userId, phase: 'pre' }, }); const userIsGuest = destIdentity.user.isGuest; const token = generateSessionToken(); await dest.authSession.create({ data: { userId: destIdentity.userId, tokenHash: hashSessionToken(token), expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); const auth = { Authorization: `Bearer ${token}` }; const loginWrong = await requestJson('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: TARGET_EMAIL, password: 'definitely-not-the-password' }), }); const loginPassword = process.env.RECOVERY_LOGIN_PASSWORD; let loginOk: { status: number; code: string | null } | null = null; if (loginPassword) { const loginRes = await requestJson('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: TARGET_EMAIL, password: loginPassword }), }); loginOk = { status: loginRes.status, code: ((loginRes.body as { error?: { code?: string } } | null)?.error?.code) ?? null, }; } const me = await requestJson('/auth/me', { headers: auth }); const usersMe = await requestJson('/users/me', { headers: auth }); const latestPre = await requestJson('/assessments/attempts/latest?phase=pre', { headers: auth }); const journey = await requestJson('/journey', { headers: auth }); const sessionProgress = await requestJson('/sessions/progress', { headers: auth }); const richestChat = [...chatSessions].sort((a, b) => b._count.messages - a._count.messages)[0]; const firstChatId = richestChat?.id.toString(); const chatMessages = firstChatId ? await requestJson(`/chat/sessions/${firstChatId}/messages`, { headers: auth }) : { status: 0, body: null }; const meData = dataOf(me.body); const usersData = dataOf(usersMe.body); const journeyData = dataOf(journey.body); const progressData = dataOf(sessionProgress.body); const chatData = dataOf(chatMessages.body); const latestData = dataOf(latestPre.body); const chatOwned = Boolean(firstChatId) && Array.isArray((chatData as { messages?: unknown[] } | null)?.messages) && ((chatData as { messages: unknown[] }).messages.length > 0); console.log( JSON.stringify( { email: TARGET_EMAIL, externalId: destIdentity.user.externalId, hashMatch, hashIsArgon2: Boolean(destIdentity.passwordHash?.startsWith('$argon2id$')), profilePresent: Boolean(destIdentity.user.profile), preAttempts: attempts, progress, chatSessions: chatSessions.map((row) => ({ id: row.id.toString(), sessionOrder: row.sessionOrder, messageCount: row._count.messages, })), loginWrongStatus: loginWrong.status, loginWithProvidedPassword: loginOk, userIsGuest, meStatus: me.status, meEmailMatches: meData?.email === TARGET_EMAIL, meIsRegistered: meData?.isGuest === false, meProfileComplete: meData?.profileComplete === true, usersMeStatus: usersMe.status, usersProfilePresent: Boolean( usersData?.profile && typeof usersData.profile === 'object', ), latestPreStatus: latestPre.status, latestPrePhase: latestData?.phase ?? null, journeyStatus: journey.status, journeyStep: journeyData?.currentStep ?? null, journeyPreSet: Boolean(journeyData?.preTestCompletedAt), progressStatus: sessionProgress.status, currentSessionOrder: progressData?.currentSessionOrder ?? null, chatHistoryStatus: chatMessages.status, chatHistoryCount: Array.isArray((chatData as { messages?: unknown[] } | null)?.messages) ? (chatData as { messages: unknown[] }).messages.length : 0, chatOwnedByUser: chatOwned, }, null, 2, ), ); } finally { await dest.$disconnect(); await source.$disconnect(); } } main().catch((err) => { console.error(err instanceof Error ? err.message : err); process.exit(1); });