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 { NextRequest, NextResponse } from 'next/server';
|
||||||
import { requireAuth, verifyPassword } from '@/lib/auth';
|
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 { eq } from 'drizzle-orm';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import { decryptApiKey, deserializeEncryptedData } from '@/lib/bots/encryption';
|
||||||
|
|
||||||
// We'll use a simple in-memory zip approach
|
// We'll use a simple in-memory zip approach
|
||||||
// For production, consider using a streaming zip library
|
// For production, consider using a streaming zip library
|
||||||
@@ -51,6 +52,47 @@ interface ExportFollowing {
|
|||||||
avatarUrl?: string | null;
|
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
|
* 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),
|
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
|
// Build export data
|
||||||
const exportPosts: ExportPost[] = userPosts.map(post => ({
|
const exportPosts: ExportPost[] = userPosts.map(post => ({
|
||||||
id: post.id,
|
id: post.id,
|
||||||
@@ -177,6 +240,75 @@ export async function POST(req: NextRequest) {
|
|||||||
headerUrl: user.headerUrl,
|
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
|
// Encrypt private key
|
||||||
const privateKey = user.privateKeyEncrypted || '';
|
const privateKey = user.privateKeyEncrypted || '';
|
||||||
const { encrypted, salt, iv } = encryptPrivateKey(privateKey, password);
|
const { encrypted, salt, iv } = encryptPrivateKey(privateKey, password);
|
||||||
@@ -205,8 +337,8 @@ export async function POST(req: NextRequest) {
|
|||||||
profile,
|
profile,
|
||||||
posts: exportPosts,
|
posts: exportPosts,
|
||||||
following: exportFollowing,
|
following: exportFollowing,
|
||||||
// Media URLs are included in posts, client can download them separately
|
dms: exportDMs,
|
||||||
// For full ZIP export, we'd need to fetch and bundle media files
|
bots: exportBots,
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -215,6 +347,8 @@ export async function POST(req: NextRequest) {
|
|||||||
stats: {
|
stats: {
|
||||||
posts: exportPosts.length,
|
posts: exportPosts.length,
|
||||||
following: exportFollowing.length,
|
following: exportFollowing.length,
|
||||||
|
dms: exportDMs.length,
|
||||||
|
bots: exportBots.length,
|
||||||
mediaFiles: exportPosts.reduce((sum, p) => sum + p.media.length, 0),
|
mediaFiles: exportPosts.reduce((sum, p) => sum + p.media.length, 0),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
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 { eq } from 'drizzle-orm';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import { encryptApiKey, serializeEncryptedData } from '@/lib/bots/encryption';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { upsertHandleEntries } from '@/lib/federation/handles';
|
import { upsertHandleEntries } from '@/lib/federation/handles';
|
||||||
|
|
||||||
@@ -45,11 +46,54 @@ interface ImportFollowing {
|
|||||||
handle: string;
|
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 {
|
interface ImportPackage {
|
||||||
manifest: ImportManifest;
|
manifest: ImportManifest;
|
||||||
profile: ImportProfile;
|
profile: ImportProfile;
|
||||||
posts: ImportPost[];
|
posts: ImportPost[];
|
||||||
following: ImportFollowing[];
|
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 });
|
return NextResponse.json({ error: 'Unsupported export version' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dms: importDMs = [], bots: importBots = [] } = exportData;
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
if (!verifyManifestSignature(manifest)) {
|
if (!verifyManifestSignature(manifest)) {
|
||||||
return NextResponse.json({ error: 'Invalid export signature' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid export signature' }, { status: 400 });
|
||||||
@@ -198,9 +244,9 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
if (node?.isNsfw) {
|
if (node?.isNsfw) {
|
||||||
await db.update(users)
|
await db.update(users)
|
||||||
.set({
|
.set({
|
||||||
nsfwEnabled: true,
|
nsfwEnabled: true,
|
||||||
isNsfw: true
|
isNsfw: true
|
||||||
})
|
})
|
||||||
.where(eq(users.id, newUser.id));
|
.where(eq(users.id, newUser.id));
|
||||||
}
|
}
|
||||||
@@ -241,6 +287,105 @@ export async function POST(req: NextRequest) {
|
|||||||
updatedAt: new Date().toISOString(),
|
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
|
// Notify old node about the migration
|
||||||
try {
|
try {
|
||||||
await notifyOldNode(manifest.sourceNode, manifest.handle, newActorUrl, manifest.did, privateKey);
|
await notifyOldNode(manifest.sourceNode, manifest.handle, newActorUrl, manifest.did, privateKey);
|
||||||
@@ -260,6 +405,8 @@ export async function POST(req: NextRequest) {
|
|||||||
stats: {
|
stats: {
|
||||||
postsImported: importedPosts,
|
postsImported: importedPosts,
|
||||||
followingToRestore: following.length,
|
followingToRestore: following.length,
|
||||||
|
dmsImported: importDMs.length,
|
||||||
|
botsImported: importBots.length,
|
||||||
},
|
},
|
||||||
message: 'Account imported successfully. Your followers on other Synapsis nodes will be automatically migrated.',
|
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();
|
const session = await getSession();
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.json({ isAdmin: false, user: null });
|
return NextResponse.json({ isAdmin: false, user: null });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ export async function POST(request: NextRequest) {
|
|||||||
senderNodeDomain: senderNodeDomain,
|
senderNodeDomain: senderNodeDomain,
|
||||||
senderDid: did,
|
senderDid: did,
|
||||||
content: content,
|
content: content,
|
||||||
encryptedContent: '',
|
|
||||||
deliveredAt: new Date(),
|
deliveredAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,6 @@ export async function POST(request: NextRequest) {
|
|||||||
senderNodeDomain: null,
|
senderNodeDomain: null,
|
||||||
senderDid: user.did,
|
senderDid: user.did,
|
||||||
content: content,
|
content: content,
|
||||||
// Encrypted fields are null for plain text chat
|
|
||||||
encryptedContent: '',
|
|
||||||
deliveredAt: new Date(),
|
deliveredAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +131,6 @@ export async function POST(request: NextRequest) {
|
|||||||
senderNodeDomain: null,
|
senderNodeDomain: null,
|
||||||
senderDid: user.did,
|
senderDid: user.did,
|
||||||
content: content,
|
content: content,
|
||||||
encryptedContent: '',
|
|
||||||
deliveredAt: new Date(),
|
deliveredAt: new Date(),
|
||||||
readAt: new Date() // Sender has read their own message
|
readAt: new Date() // Sender has read their own message
|
||||||
});
|
});
|
||||||
@@ -217,7 +214,6 @@ export async function POST(request: NextRequest) {
|
|||||||
senderNodeDomain: null,
|
senderNodeDomain: null,
|
||||||
senderDid: user.did,
|
senderDid: user.did,
|
||||||
content: content,
|
content: content,
|
||||||
encryptedContent: '',
|
|
||||||
deliveredAt: new Date(),
|
deliveredAt: new Date(),
|
||||||
readAt: 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;
|
posts: number;
|
||||||
following: number;
|
following: number;
|
||||||
mediaFiles: number;
|
mediaFiles: number;
|
||||||
|
dms: number;
|
||||||
|
bots: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MigrationPage() {
|
export default function MigrationPage() {
|
||||||
@@ -137,6 +139,8 @@ export default function MigrationPage() {
|
|||||||
<li>Your profile information</li>
|
<li>Your profile information</li>
|
||||||
<li>All your posts</li>
|
<li>All your posts</li>
|
||||||
<li>Your following list</li>
|
<li>Your following list</li>
|
||||||
|
<li>All DMs and conversation history</li>
|
||||||
|
<li>Your automated bots and their configuration</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,7 +171,7 @@ export default function MigrationPage() {
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
marginBottom: '20px',
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user