Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
173 lines
6.5 KiB
TypeScript
173 lines
6.5 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|