From 9f9507b784481180a316f8366580901e97574e56 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 27 Apr 2025 16:36:09 +0200 Subject: [PATCH] courrier multi account --- app/api/admin/restore-credentials/route.ts | 133 ++++++++++++++++++ app/api/admin/view-redis-credentials/route.ts | 65 +++++++++ app/api/courrier/account/route.ts | 45 +++++- app/courrier/page.tsx | 103 ++++++++------ 4 files changed, 299 insertions(+), 47 deletions(-) create mode 100644 app/api/admin/restore-credentials/route.ts create mode 100644 app/api/admin/view-redis-credentials/route.ts diff --git a/app/api/admin/restore-credentials/route.ts b/app/api/admin/restore-credentials/route.ts new file mode 100644 index 00000000..d6784455 --- /dev/null +++ b/app/api/admin/restore-credentials/route.ts @@ -0,0 +1,133 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { prisma } from '@/lib/prisma'; +import { getRedisClient } from '@/lib/redis'; + +// This is an admin-only route to restore email credentials from Redis to the database +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Only allow authorized users + if (!session.user.role.includes('admin')) { + return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); + } + + const redis = getRedisClient(); + + // Get all email credential keys + const keys = await redis.keys('email:credentials:*'); + console.log(`Found ${keys.length} credential records in Redis`); + + const results = { + total: keys.length, + processed: 0, + success: 0, + errors: [] as string[] + }; + + // Process each key + for (const key of keys) { + results.processed++; + + try { + // Extract user ID from key + const userId = key.split(':')[2]; + if (!userId) { + results.errors.push(`Invalid key format: ${key}`); + continue; + } + + // Get credentials from Redis + const credStr = await redis.get(key); + if (!credStr) { + results.errors.push(`No data found for key: ${key}`); + continue; + } + + // Parse credentials + const creds = JSON.parse(credStr); + console.log(`Processing credentials for user ${userId}`, { + email: creds.email, + host: creds.host + }); + + // Check if user exists + const userExists = await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true } + }); + + if (!userExists) { + // Create dummy user if needed (this is optional and might not be appropriate in all cases) + // Remove or modify this section if you don't want to create placeholder users + console.log(`User ${userId} not found, creating placeholder`); + await prisma.user.create({ + data: { + id: userId, + email: creds.email || 'placeholder@example.com', + password: 'PLACEHOLDER_HASH_CHANGE_THIS', // You should set a proper password + // Add any other required fields + } + }); + console.log(`Created placeholder user ${userId}`); + } + + // Upsert credentials in database + await prisma.mailCredentials.upsert({ + where: { userId }, + update: { + email: creds.email, + password: creds.encryptedPassword || 'encrypted_placeholder', + host: creds.host, + port: creds.port, + // Optional fields + ...(creds.secure !== undefined && { secure: creds.secure }), + ...(creds.smtp_host && { smtp_host: creds.smtp_host }), + ...(creds.smtp_port && { smtp_port: creds.smtp_port }), + ...(creds.smtp_secure !== undefined && { smtp_secure: creds.smtp_secure }), + ...(creds.display_name && { display_name: creds.display_name }), + ...(creds.color && { color: creds.color }) + }, + create: { + userId, + email: creds.email, + password: creds.encryptedPassword || 'encrypted_placeholder', + host: creds.host, + port: creds.port, + // Optional fields + ...(creds.secure !== undefined && { secure: creds.secure }), + ...(creds.smtp_host && { smtp_host: creds.smtp_host }), + ...(creds.smtp_port && { smtp_port: creds.smtp_port }), + ...(creds.smtp_secure !== undefined && { smtp_secure: creds.smtp_secure }), + ...(creds.display_name && { display_name: creds.display_name }), + ...(creds.color && { color: creds.color }) + } + }); + + results.success++; + console.log(`Successfully restored credentials for user ${userId}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + results.errors.push(`Error processing ${key}: ${message}`); + console.error(`Error processing ${key}:`, error); + } + } + + return NextResponse.json({ + message: 'Credential restoration process completed', + results + }); + } catch (error) { + console.error('Error in restore credentials route:', error); + return NextResponse.json( + { error: 'Failed to restore credentials', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/admin/view-redis-credentials/route.ts b/app/api/admin/view-redis-credentials/route.ts new file mode 100644 index 00000000..62b1654b --- /dev/null +++ b/app/api/admin/view-redis-credentials/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getRedisClient } from '@/lib/redis'; + +// This route just views Redis email credentials without making any changes +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const redis = getRedisClient(); + + // Get all email credential keys + const keys = await redis.keys('email:credentials:*'); + console.log(`Found ${keys.length} credential records in Redis`); + + const credentials = []; + + // Process each key + for (const key of keys) { + try { + // Extract user ID from key + const userId = key.split(':')[2]; + + // Get credentials from Redis + const credStr = await redis.get(key); + if (!credStr) continue; + + // Parse credentials + const creds = JSON.parse(credStr); + + // Add to results (remove sensitive data) + credentials.push({ + userId, + email: creds.email, + host: creds.host, + port: creds.port, + hasPassword: !!creds.encryptedPassword, + // Include other non-sensitive fields + smtp_host: creds.smtp_host, + smtp_port: creds.smtp_port, + display_name: creds.display_name, + color: creds.color + }); + } catch (error) { + console.error(`Error processing ${key}:`, error); + } + } + + return NextResponse.json({ + count: credentials.length, + credentials + }); + } catch (error) { + console.error('Error viewing Redis credentials:', error); + return NextResponse.json( + { error: 'Failed to view credentials', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/courrier/account/route.ts b/app/api/courrier/account/route.ts index d7736ba0..8d96f6bb 100644 --- a/app/api/courrier/account/route.ts +++ b/app/api/courrier/account/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { saveUserEmailCredentials } from '@/lib/services/email-service'; +import { saveUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service'; +import { prisma } from '@/lib/prisma'; // Define EmailCredentials interface inline since we're having import issues interface EmailCredentials { @@ -17,6 +18,22 @@ interface EmailCredentials { color?: string; } +/** + * Check if a user exists in the database + */ +async function userExists(userId: string): Promise { + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true } + }); + return !!user; + } catch (error) { + console.error(`Error checking if user exists:`, error); + return false; + } +} + export async function POST(request: Request) { try { // Authenticate user @@ -28,6 +45,19 @@ export async function POST(request: Request) { ); } + // Verify that the user exists in the database + const userExistsInDB = await userExists(session.user.id); + if (!userExistsInDB) { + console.error(`User with ID ${session.user.id} not found in database`); + return NextResponse.json( + { + error: 'User not found in database', + details: `The user ID from your session (${session.user.id}) doesn't exist in the database. This may be due to a session/database mismatch.` + }, + { status: 400 } + ); + } + // Parse request body const body = await request.json().catch(e => { console.error('Error parsing request body:', e); @@ -92,8 +122,19 @@ export async function POST(request: Request) { ...(color && { color }) }; - // Connection test is no longer needed here since we validate with the test-connection endpoint first + // Test connection before saving + console.log(`Testing connection before saving for user ${session.user.id}`); + const testResult = await testEmailConnection(credentials); + + if (!testResult.imap) { + return NextResponse.json( + { error: `Connection test failed: ${testResult.error || 'Could not connect to IMAP server'}` }, + { status: 400 } + ); + } + // Save credentials to database and cache + console.log(`Saving credentials for user: ${session.user.id}`); await saveUserEmailCredentials(session.user.id, credentials); console.log(`Email account successfully added for user ${session.user.id}`); diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 7ba319c1..6af03e2c 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -428,26 +428,28 @@ export default function CourrierPage() { - {/* Accounts Section */} -
-
- - -
+ {/* Scrollable area for accounts and folders */} + + {/* Accounts Section */} +
+
+ + +
{/* Form for adding a new account */} {showAddAccountForm && ( @@ -656,9 +658,11 @@ export default function CourrierPage() { variant="ghost" className="w-full justify-between px-2 py-1.5 text-sm group" onClick={() => { - if (account.id !== 0) { - // Only toggle folders for non-All accounts + // Toggle folders for this specific account + if (selectedAccount?.id === account.id) { setShowFolders(!showFolders); + } else { + setShowFolders(true); } setSelectedAccount(account); }} @@ -667,18 +671,17 @@ export default function CourrierPage() {
{account.name}
- {account.id !== 0 && ( -
- {showFolders ? - : - - } -
- )} + {/* Show arrow for all accounts */} +
+ {selectedAccount?.id === account.id && showFolders ? + : + + } +
- {/* Show folders for email accounts (not for "All" account) without the "Folders" header */} - {account.id !== 0 && showFolders && ( + {/* Show folders for this account if it's selected and folders are shown */} + {selectedAccount?.id === account.id && showFolders && account.folders && (
{account.folders && account.folders.length > 0 ? ( account.folders.map((folder) => ( @@ -699,7 +702,7 @@ export default function CourrierPage() { {formatFolderName(folder)}
{folder === 'INBOX' && unreadCount > 0 && ( - + {unreadCount} )} @@ -707,17 +710,7 @@ export default function CourrierPage() { )) ) : ( -
-
- {/* Create placeholder folder items with shimmer effect */} - {Array.from({ length: 5 }).map((_, index) => ( -
-
-
-
- ))} -
-
+
No folders found
)}
)} @@ -725,7 +718,27 @@ export default function CourrierPage() { ))} )} - + + + {/* Categories Section */} +
+

Categories

+
+ +
+
+ {/* Email List and Content View */}