Files
pwa/backend/scripts/verify-recovered-user.ts
T

173 lines
6.5 KiB
TypeScript
Raw Normal View History

/**
* 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<string, unknown> | null {
if (!body || typeof body !== 'object') return null;
const record = body as Record<string, unknown>;
if (record.data && typeof record.data === 'object') {
return record.data as Record<string, unknown>;
}
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);
});