feat: Remove chat key management and debug endpoints, simplify chat message handling, and enhance migration page with DM and bot export capabilities.
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { db } from '../src/db';
|
||||
import { chatDeviceBundles } from '../src/db/schema';
|
||||
|
||||
async function clearChatKeys() {
|
||||
console.log('Clearing all chat device bundles...');
|
||||
|
||||
const result = await db.delete(chatDeviceBundles);
|
||||
|
||||
console.log('Cleared chat device bundles');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
clearChatKeys().catch((error) => {
|
||||
console.error('Failed to clear keys:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { chatDeviceBundles } from '@/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ did: string }> }
|
||||
) {
|
||||
const { did } = await params;
|
||||
|
||||
// Fetch device bundle for this DID
|
||||
const bundle = await db.query.chatDeviceBundles.findFirst({
|
||||
where: eq(chatDeviceBundles.did, did),
|
||||
});
|
||||
|
||||
if (!bundle) {
|
||||
return NextResponse.json({ error: 'No keys found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const signedPreKey = JSON.parse(bundle.signedPreKey);
|
||||
const kyberPreKey = bundle.kyberPreKey ? JSON.parse(bundle.kyberPreKey) : null;
|
||||
|
||||
// Format Response for Olm
|
||||
const response = {
|
||||
identityKey: bundle.identityKey, // curve25519
|
||||
signingKey: signedPreKey.signingKey, // ed25519
|
||||
oneTimeKeys: kyberPreKey?.oneTimeKeys || [],
|
||||
};
|
||||
|
||||
return NextResponse.json(response, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Cache-Control': 'max-age=60'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new NextResponse(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -7,9 +7,10 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAuth, verifyPassword } from '@/lib/auth';
|
||||
import { db, posts, media, follows, users, remoteFollows } from '@/db';
|
||||
import { db, posts, media, follows, users, remoteFollows, chatConversations, chatMessages, bots, botContentSources, botActivityLogs } from '@/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import * as crypto from 'crypto';
|
||||
import { decryptApiKey, deserializeEncryptedData } from '@/lib/bots/encryption';
|
||||
|
||||
// We'll use a simple in-memory zip approach
|
||||
// For production, consider using a streaming zip library
|
||||
@@ -51,6 +52,47 @@ interface ExportFollowing {
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
interface ExportDMConversation {
|
||||
id: string;
|
||||
type: string;
|
||||
participant2Handle: string;
|
||||
lastMessageAt: string | null;
|
||||
lastMessagePreview: string | null;
|
||||
messages: ExportDMMessage[];
|
||||
}
|
||||
|
||||
interface ExportDMMessage {
|
||||
senderHandle: string;
|
||||
senderDisplayName: string | null;
|
||||
senderAvatarUrl: string | null;
|
||||
senderNodeDomain: string | null;
|
||||
senderDid: string | null;
|
||||
content: string | null;
|
||||
deliveredAt: string | null;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ExportBot {
|
||||
id: string;
|
||||
name: string;
|
||||
handle: string;
|
||||
bio: string | null;
|
||||
avatarUrl: string | null;
|
||||
headerUrl: string | null;
|
||||
personalityConfig: any;
|
||||
llmProvider: string;
|
||||
llmModel: string;
|
||||
llmApiKey: string; // Decrypted
|
||||
botPrivateKey: string; // Decrypted
|
||||
publicKey: string;
|
||||
scheduleConfig: any;
|
||||
autonomousMode: boolean;
|
||||
isActive: boolean;
|
||||
sources: any[];
|
||||
activityLogs: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the private key with user's password using AES-256-GCM
|
||||
*/
|
||||
@@ -136,6 +178,27 @@ export async function POST(req: NextRequest) {
|
||||
where: eq(remoteFollows.followerId, user.id),
|
||||
});
|
||||
|
||||
// Fetch DMs
|
||||
const userConversations = await db.query.chatConversations.findMany({
|
||||
where: eq(chatConversations.participant1Id, user.id),
|
||||
with: {
|
||||
messages: true
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Bots
|
||||
const userBots = await db.query.bots.findMany({
|
||||
where: eq(bots.ownerId, user.id),
|
||||
with: {
|
||||
user: true,
|
||||
contentSources: true,
|
||||
activityLogs: {
|
||||
limit: 50,
|
||||
orderBy: (logs, { desc }) => [desc(logs.createdAt)]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build export data
|
||||
const exportPosts: ExportPost[] = userPosts.map(post => ({
|
||||
id: post.id,
|
||||
@@ -177,6 +240,75 @@ export async function POST(req: NextRequest) {
|
||||
headerUrl: user.headerUrl,
|
||||
};
|
||||
|
||||
const exportDMs: ExportDMConversation[] = userConversations.map(conv => ({
|
||||
id: conv.id,
|
||||
type: conv.type,
|
||||
participant2Handle: conv.participant2Handle,
|
||||
lastMessageAt: conv.lastMessageAt?.toISOString() || null,
|
||||
lastMessagePreview: conv.lastMessagePreview,
|
||||
messages: conv.messages.map(msg => ({
|
||||
senderHandle: msg.senderHandle,
|
||||
senderDisplayName: msg.senderDisplayName,
|
||||
senderAvatarUrl: msg.senderAvatarUrl,
|
||||
senderNodeDomain: msg.senderNodeDomain,
|
||||
senderDid: msg.senderDid,
|
||||
content: msg.content,
|
||||
deliveredAt: msg.deliveredAt?.toISOString() || null,
|
||||
readAt: msg.readAt?.toISOString() || null,
|
||||
createdAt: msg.createdAt.toISOString()
|
||||
}))
|
||||
}));
|
||||
|
||||
const exportBots: ExportBot[] = userBots.map(bot => {
|
||||
// Decrypt bot keys
|
||||
let llmApiKey = '';
|
||||
let botPrivateKey = '';
|
||||
try {
|
||||
if (bot.llmApiKeyEncrypted) {
|
||||
llmApiKey = decryptApiKey(deserializeEncryptedData(bot.llmApiKeyEncrypted));
|
||||
}
|
||||
if (bot.user?.privateKeyEncrypted) {
|
||||
botPrivateKey = decryptApiKey(deserializeEncryptedData(bot.user.privateKeyEncrypted));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to decrypt keys for bot ${bot.name}:`, e);
|
||||
}
|
||||
|
||||
return {
|
||||
id: bot.id,
|
||||
name: bot.name,
|
||||
handle: bot.user.handle,
|
||||
bio: bot.user.bio,
|
||||
avatarUrl: bot.user.avatarUrl,
|
||||
headerUrl: bot.user.headerUrl,
|
||||
personalityConfig: JSON.parse(bot.personalityConfig),
|
||||
llmProvider: bot.llmProvider,
|
||||
llmModel: bot.llmModel,
|
||||
llmApiKey,
|
||||
botPrivateKey,
|
||||
publicKey: bot.user.publicKey,
|
||||
scheduleConfig: bot.scheduleConfig ? JSON.parse(bot.scheduleConfig) : null,
|
||||
autonomousMode: bot.autonomousMode,
|
||||
isActive: bot.isActive,
|
||||
sources: bot.contentSources.map(s => ({
|
||||
type: s.type,
|
||||
url: s.url,
|
||||
subreddit: s.subreddit,
|
||||
apiKeyEncrypted: s.apiKeyEncrypted, // These are specific to sources and might need their own decryption if they use AUTH_SECRET
|
||||
sourceConfig: s.sourceConfig ? JSON.parse(s.sourceConfig) : null,
|
||||
keywords: s.keywords ? JSON.parse(s.keywords) : null,
|
||||
isActive: s.isActive
|
||||
})),
|
||||
activityLogs: bot.activityLogs.map(l => ({
|
||||
action: l.action,
|
||||
details: JSON.parse(l.details),
|
||||
success: l.success,
|
||||
errorMessage: l.errorMessage,
|
||||
createdAt: l.createdAt.toISOString()
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
// Encrypt private key
|
||||
const privateKey = user.privateKeyEncrypted || '';
|
||||
const { encrypted, salt, iv } = encryptPrivateKey(privateKey, password);
|
||||
@@ -205,8 +337,8 @@ export async function POST(req: NextRequest) {
|
||||
profile,
|
||||
posts: exportPosts,
|
||||
following: exportFollowing,
|
||||
// Media URLs are included in posts, client can download them separately
|
||||
// For full ZIP export, we'd need to fetch and bundle media files
|
||||
dms: exportDMs,
|
||||
bots: exportBots,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -215,6 +347,8 @@ export async function POST(req: NextRequest) {
|
||||
stats: {
|
||||
posts: exportPosts.length,
|
||||
following: exportFollowing.length,
|
||||
dms: exportDMs.length,
|
||||
bots: exportBots.length,
|
||||
mediaFiles: exportPosts.reduce((sum, p) => sum + p.media.length, 0),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db, users, posts, media, follows, nodes } from '@/db';
|
||||
import { db, users, posts, media, follows, nodes, chatConversations, chatMessages, bots, botContentSources, botActivityLogs } from '@/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import * as crypto from 'crypto';
|
||||
import { encryptApiKey, serializeEncryptedData } from '@/lib/bots/encryption';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { upsertHandleEntries } from '@/lib/federation/handles';
|
||||
|
||||
@@ -45,11 +46,54 @@ interface ImportFollowing {
|
||||
handle: string;
|
||||
}
|
||||
|
||||
interface ImportDMConversation {
|
||||
id: string;
|
||||
type: string;
|
||||
participant2Handle: string;
|
||||
lastMessageAt: string | null;
|
||||
lastMessagePreview: string | null;
|
||||
messages: ImportDMMessage[];
|
||||
}
|
||||
|
||||
interface ImportDMMessage {
|
||||
senderHandle: string;
|
||||
senderDisplayName: string | null;
|
||||
senderAvatarUrl: string | null;
|
||||
senderNodeDomain: string | null;
|
||||
senderDid: string | null;
|
||||
content: string | null;
|
||||
deliveredAt: string | null;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ImportBot {
|
||||
id: string;
|
||||
name: string;
|
||||
handle: string;
|
||||
bio: string | null;
|
||||
avatarUrl: string | null;
|
||||
headerUrl: string | null;
|
||||
personalityConfig: any;
|
||||
llmProvider: string;
|
||||
llmModel: string;
|
||||
llmApiKey: string;
|
||||
botPrivateKey: string;
|
||||
publicKey: string;
|
||||
scheduleConfig: any;
|
||||
autonomousMode: boolean;
|
||||
isActive: boolean;
|
||||
sources: any[];
|
||||
activityLogs: any[];
|
||||
}
|
||||
|
||||
interface ImportPackage {
|
||||
manifest: ImportManifest;
|
||||
profile: ImportProfile;
|
||||
posts: ImportPost[];
|
||||
following: ImportFollowing[];
|
||||
dms: ImportDMConversation[];
|
||||
bots: ImportBot[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,6 +167,8 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Unsupported export version' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { dms: importDMs = [], bots: importBots = [] } = exportData;
|
||||
|
||||
// Verify signature
|
||||
if (!verifyManifestSignature(manifest)) {
|
||||
return NextResponse.json({ error: 'Invalid export signature' }, { status: 400 });
|
||||
@@ -198,9 +244,9 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
if (node?.isNsfw) {
|
||||
await db.update(users)
|
||||
.set({
|
||||
.set({
|
||||
nsfwEnabled: true,
|
||||
isNsfw: true
|
||||
isNsfw: true
|
||||
})
|
||||
.where(eq(users.id, newUser.id));
|
||||
}
|
||||
@@ -241,6 +287,105 @@ export async function POST(req: NextRequest) {
|
||||
updatedAt: new Date().toISOString(),
|
||||
}]);
|
||||
|
||||
// Import DMs
|
||||
for (const conv of importDMs) {
|
||||
try {
|
||||
const [newConv] = await db.insert(chatConversations).values({
|
||||
participant1Id: newUser.id,
|
||||
participant2Handle: conv.participant2Handle,
|
||||
type: conv.type,
|
||||
lastMessageAt: conv.lastMessageAt ? new Date(conv.lastMessageAt) : null,
|
||||
lastMessagePreview: conv.lastMessagePreview
|
||||
}).returning();
|
||||
|
||||
for (const msg of conv.messages) {
|
||||
await db.insert(chatMessages).values({
|
||||
conversationId: newConv.id,
|
||||
senderHandle: msg.senderHandle,
|
||||
senderDisplayName: msg.senderDisplayName,
|
||||
senderAvatarUrl: msg.senderAvatarUrl,
|
||||
senderNodeDomain: msg.senderNodeDomain,
|
||||
senderDid: msg.senderDid,
|
||||
content: msg.content,
|
||||
deliveredAt: msg.deliveredAt ? new Date(msg.deliveredAt) : null,
|
||||
readAt: msg.readAt ? new Date(msg.readAt) : null,
|
||||
createdAt: new Date(msg.createdAt)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to import DM conversation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Import Bots
|
||||
for (const botData of importBots) {
|
||||
try {
|
||||
// 1. Create Bot User Account
|
||||
const botDid = `did:web:${nodeDomain}:users:${botData.handle.toLowerCase()}`;
|
||||
|
||||
// Re-encrypt bot private key
|
||||
const encryptedBotPrivKey = encryptApiKey(botData.botPrivateKey);
|
||||
|
||||
const [botUser] = await db.insert(users).values({
|
||||
did: botDid,
|
||||
handle: botData.handle.toLowerCase(),
|
||||
displayName: botData.name,
|
||||
bio: botData.bio,
|
||||
avatarUrl: botData.avatarUrl,
|
||||
headerUrl: botData.headerUrl,
|
||||
publicKey: botData.publicKey,
|
||||
privateKeyEncrypted: serializeEncryptedData(encryptedBotPrivKey),
|
||||
isBot: true,
|
||||
botOwnerId: newUser.id
|
||||
}).returning();
|
||||
|
||||
// 2. Create Bot Config
|
||||
const encryptedLlmKey = encryptApiKey(botData.llmApiKey);
|
||||
|
||||
const [newBot] = await db.insert(bots).values({
|
||||
userId: botUser.id,
|
||||
ownerId: newUser.id,
|
||||
name: botData.name,
|
||||
personalityConfig: JSON.stringify(botData.personalityConfig),
|
||||
llmProvider: botData.llmProvider,
|
||||
llmModel: botData.llmModel,
|
||||
llmApiKeyEncrypted: serializeEncryptedData(encryptedLlmKey),
|
||||
scheduleConfig: botData.scheduleConfig ? JSON.stringify(botData.scheduleConfig) : null,
|
||||
autonomousMode: botData.autonomousMode,
|
||||
isActive: botData.isActive
|
||||
}).returning();
|
||||
|
||||
// 3. Import Sources
|
||||
for (const source of botData.sources) {
|
||||
await db.insert(botContentSources).values({
|
||||
botId: newBot.id,
|
||||
type: source.type,
|
||||
url: source.url,
|
||||
subreddit: source.subreddit,
|
||||
apiKeyEncrypted: source.apiKeyEncrypted,
|
||||
sourceConfig: source.sourceConfig ? JSON.stringify(source.sourceConfig) : null,
|
||||
keywords: source.keywords ? JSON.stringify(source.keywords) : null,
|
||||
isActive: source.isActive
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Import Activity Logs
|
||||
for (const log of botData.activityLogs) {
|
||||
await db.insert(botActivityLogs).values({
|
||||
botId: newBot.id,
|
||||
action: log.action,
|
||||
details: JSON.stringify(log.details),
|
||||
success: log.success,
|
||||
errorMessage: log.errorMessage,
|
||||
createdAt: new Date(log.createdAt)
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to import bot ${botData.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify old node about the migration
|
||||
try {
|
||||
await notifyOldNode(manifest.sourceNode, manifest.handle, newActorUrl, manifest.did, privateKey);
|
||||
@@ -260,6 +405,8 @@ export async function POST(req: NextRequest) {
|
||||
stats: {
|
||||
postsImported: importedPosts,
|
||||
followingToRestore: following.length,
|
||||
dmsImported: importDMs.length,
|
||||
botsImported: importBots.length,
|
||||
},
|
||||
message: 'Account imported successfully. Your followers on other Synapsis nodes will be automatically migrated.',
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET() {
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ isAdmin: false, user: null });
|
||||
}
|
||||
|
||||
@@ -134,7 +134,6 @@ export async function POST(request: NextRequest) {
|
||||
senderNodeDomain: senderNodeDomain,
|
||||
senderDid: did,
|
||||
content: content,
|
||||
encryptedContent: '',
|
||||
deliveredAt: new Date(),
|
||||
});
|
||||
|
||||
|
||||
@@ -119,8 +119,6 @@ export async function POST(request: NextRequest) {
|
||||
senderNodeDomain: null,
|
||||
senderDid: user.did,
|
||||
content: content,
|
||||
// Encrypted fields are null for plain text chat
|
||||
encryptedContent: '',
|
||||
deliveredAt: new Date(),
|
||||
});
|
||||
|
||||
@@ -133,7 +131,6 @@ export async function POST(request: NextRequest) {
|
||||
senderNodeDomain: null,
|
||||
senderDid: user.did,
|
||||
content: content,
|
||||
encryptedContent: '',
|
||||
deliveredAt: new Date(),
|
||||
readAt: new Date() // Sender has read their own message
|
||||
});
|
||||
@@ -217,7 +214,6 @@ export async function POST(request: NextRequest) {
|
||||
senderNodeDomain: null,
|
||||
senderDid: user.did,
|
||||
content: content,
|
||||
encryptedContent: '',
|
||||
deliveredAt: new Date(),
|
||||
readAt: new Date()
|
||||
});
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, users } from '@/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const allUsers = await db.select({
|
||||
id: users.id,
|
||||
handle: users.handle,
|
||||
chatPublicKey: users.chatPublicKey,
|
||||
chatPrivateKeyEncrypted: users.chatPrivateKeyEncrypted,
|
||||
}).from(users);
|
||||
|
||||
const results = allUsers.map(user => {
|
||||
let publicKeyInfo = null;
|
||||
if (user.chatPublicKey) {
|
||||
try {
|
||||
const decoded = Buffer.from(user.chatPublicKey, 'base64');
|
||||
publicKeyInfo = {
|
||||
stringLength: user.chatPublicKey.length,
|
||||
decodedBytes: decoded.byteLength,
|
||||
isValidSize: decoded.byteLength === 91,
|
||||
isCorrupted: decoded.byteLength !== 91,
|
||||
};
|
||||
} catch (e) {
|
||||
publicKeyInfo = {
|
||||
error: 'Not valid base64',
|
||||
stringLength: user.chatPublicKey.length,
|
||||
isCorrupted: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handle: user.handle,
|
||||
hasPublicKey: !!user.chatPublicKey,
|
||||
publicKeyInfo,
|
||||
encryptedPrivateKeyLength: user.chatPrivateKeyEncrypted?.length || 0,
|
||||
};
|
||||
});
|
||||
|
||||
const corrupted = results.filter(r => r.publicKeyInfo?.isCorrupted);
|
||||
|
||||
return NextResponse.json({
|
||||
total: results.length,
|
||||
withKeys: results.filter(r => r.hasPublicKey).length,
|
||||
corrupted: corrupted.length,
|
||||
corruptedUsers: corrupted.map(r => r.handle),
|
||||
allUsers: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
return NextResponse.json({ error: 'Failed to debug keys' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, users } from '@/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: (users, { eq }) => eq(users.id, session.user.id),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
let publicKeyInfo = null;
|
||||
if (user.chatPublicKey) {
|
||||
try {
|
||||
const decoded = Buffer.from(user.chatPublicKey, 'base64');
|
||||
publicKeyInfo = {
|
||||
stringLength: user.chatPublicKey.length,
|
||||
decodedBytes: decoded.byteLength,
|
||||
isValidSize: decoded.byteLength === 91,
|
||||
firstChars: user.chatPublicKey.substring(0, 20),
|
||||
};
|
||||
} catch (e) {
|
||||
publicKeyInfo = {
|
||||
error: 'Not valid base64',
|
||||
stringLength: user.chatPublicKey.length,
|
||||
firstChars: user.chatPublicKey.substring(0, 20),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
handle: user.handle,
|
||||
hasPublicKey: !!user.chatPublicKey,
|
||||
hasEncryptedPrivateKey: !!user.chatPrivateKeyEncrypted,
|
||||
publicKeyInfo,
|
||||
encryptedPrivateKeyLength: user.chatPrivateKeyEncrypted?.length || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
return NextResponse.json({ error: 'Failed to debug keys' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, chatMessages } from '@/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const messages = await db.select({
|
||||
id: chatMessages.id,
|
||||
senderHandle: chatMessages.senderHandle,
|
||||
senderChatPublicKey: chatMessages.senderChatPublicKey,
|
||||
createdAt: chatMessages.createdAt,
|
||||
}).from(chatMessages).limit(50);
|
||||
|
||||
const results = messages.map(msg => {
|
||||
let keyInfo = null;
|
||||
if (msg.senderChatPublicKey) {
|
||||
try {
|
||||
const decoded = Buffer.from(msg.senderChatPublicKey, 'base64');
|
||||
keyInfo = {
|
||||
stringLength: msg.senderChatPublicKey.length,
|
||||
decodedBytes: decoded.byteLength,
|
||||
isValidSize: decoded.byteLength === 91,
|
||||
isCorrupted: decoded.byteLength !== 91,
|
||||
firstChars: msg.senderChatPublicKey.substring(0, 20),
|
||||
};
|
||||
} catch (e) {
|
||||
keyInfo = {
|
||||
error: 'Not valid base64',
|
||||
stringLength: msg.senderChatPublicKey.length,
|
||||
isCorrupted: true,
|
||||
firstChars: msg.senderChatPublicKey.substring(0, 20),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: msg.id,
|
||||
senderHandle: msg.senderHandle,
|
||||
createdAt: msg.createdAt,
|
||||
hasSenderKey: !!msg.senderChatPublicKey,
|
||||
keyInfo,
|
||||
};
|
||||
});
|
||||
|
||||
const corrupted = results.filter(r => r.keyInfo?.isCorrupted);
|
||||
|
||||
return NextResponse.json({
|
||||
total: results.length,
|
||||
withKeys: results.filter(r => r.hasSenderKey).length,
|
||||
corrupted: corrupted.length,
|
||||
corruptedMessages: corrupted,
|
||||
allMessages: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
return NextResponse.json({ error: 'Failed to debug messages' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ interface ExportStats {
|
||||
posts: number;
|
||||
following: number;
|
||||
mediaFiles: number;
|
||||
dms: number;
|
||||
bots: number;
|
||||
}
|
||||
|
||||
export default function MigrationPage() {
|
||||
@@ -137,6 +139,8 @@ export default function MigrationPage() {
|
||||
<li>Your profile information</li>
|
||||
<li>All your posts</li>
|
||||
<li>Your following list</li>
|
||||
<li>All DMs and conversation history</li>
|
||||
<li>Your automated bots and their configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +171,7 @@ export default function MigrationPage() {
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px',
|
||||
}}>
|
||||
Export successful! Downloaded {exportStats.posts} posts and {exportStats.mediaFiles} media references.
|
||||
Export successful! Downloaded {exportStats.posts} posts, {exportStats.dms} DM threads, and {exportStats.bots} bots.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user