courrier multi account

This commit is contained in:
alma 2025-04-27 16:36:09 +02:00
parent b66f1adb3a
commit 9f9507b784
4 changed files with 299 additions and 47 deletions

View File

@ -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 }
);
}
}

View File

@ -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 }
);
}
}

View File

@ -1,7 +1,8 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; 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 // Define EmailCredentials interface inline since we're having import issues
interface EmailCredentials { interface EmailCredentials {
@ -17,6 +18,22 @@ interface EmailCredentials {
color?: string; color?: string;
} }
/**
* Check if a user exists in the database
*/
async function userExists(userId: string): Promise<boolean> {
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) { export async function POST(request: Request) {
try { try {
// Authenticate user // 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 // Parse request body
const body = await request.json().catch(e => { const body = await request.json().catch(e => {
console.error('Error parsing request body:', e); console.error('Error parsing request body:', e);
@ -92,8 +122,19 @@ export async function POST(request: Request) {
...(color && { color }) ...(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 // Save credentials to database and cache
console.log(`Saving credentials for user: ${session.user.id}`);
await saveUserEmailCredentials(session.user.id, credentials); await saveUserEmailCredentials(session.user.id, credentials);
console.log(`Email account successfully added for user ${session.user.id}`); console.log(`Email account successfully added for user ${session.user.id}`);

View File

@ -428,26 +428,28 @@ export default function CourrierPage() {
</Button> </Button>
</div> </div>
{/* Accounts Section */} {/* Scrollable area for accounts and folders */}
<div className="p-3 border-b border-gray-100"> <ScrollArea className="flex-1 h-0">
<div className="flex items-center justify-between mb-2"> {/* Accounts Section */}
<Button <div className="p-3 border-b border-gray-100">
variant="ghost" <div className="flex items-center justify-between mb-2">
className="w-full justify-between text-sm font-medium text-gray-500" <Button
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)} variant="ghost"
> className="w-full justify-between text-sm font-medium text-gray-500"
<span>Accounts</span> onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />} >
</Button> <span>Accounts</span>
<Button {accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
variant="ghost" </Button>
size="sm" <Button
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600" variant="ghost"
onClick={() => setShowAddAccountForm(!showAddAccountForm)} size="sm"
> className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
<Plus className="h-4 w-4" /> onClick={() => setShowAddAccountForm(!showAddAccountForm)}
</Button> >
</div> <Plus className="h-4 w-4" />
</Button>
</div>
{/* Form for adding a new account */} {/* Form for adding a new account */}
{showAddAccountForm && ( {showAddAccountForm && (
@ -656,9 +658,11 @@ export default function CourrierPage() {
variant="ghost" variant="ghost"
className="w-full justify-between px-2 py-1.5 text-sm group" className="w-full justify-between px-2 py-1.5 text-sm group"
onClick={() => { onClick={() => {
if (account.id !== 0) { // Toggle folders for this specific account
// Only toggle folders for non-All accounts if (selectedAccount?.id === account.id) {
setShowFolders(!showFolders); setShowFolders(!showFolders);
} else {
setShowFolders(true);
} }
setSelectedAccount(account); setSelectedAccount(account);
}} }}
@ -667,18 +671,17 @@ export default function CourrierPage() {
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div> <div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
<span className="font-medium text-gray-700 truncate">{account.name}</span> <span className="font-medium text-gray-700 truncate">{account.name}</span>
</div> </div>
{account.id !== 0 && ( {/* Show arrow for all accounts */}
<div className="flex items-center"> <div className="flex items-center">
{showFolders ? {selectedAccount?.id === account.id && showFolders ?
<ChevronDown className="h-3.5 w-3.5 text-gray-500" /> : <ChevronDown className="h-3.5 w-3.5 text-gray-500" /> :
<ChevronRight className="h-3.5 w-3.5 text-gray-500" /> <ChevronRight className="h-3.5 w-3.5 text-gray-500" />
} }
</div> </div>
)}
</Button> </Button>
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */} {/* Show folders for this account if it's selected and folders are shown */}
{account.id !== 0 && showFolders && ( {selectedAccount?.id === account.id && showFolders && account.folders && (
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200"> <div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200">
{account.folders && account.folders.length > 0 ? ( {account.folders && account.folders.length > 0 ? (
account.folders.map((folder) => ( account.folders.map((folder) => (
@ -699,7 +702,7 @@ export default function CourrierPage() {
<span className="truncate">{formatFolderName(folder)}</span> <span className="truncate">{formatFolderName(folder)}</span>
</div> </div>
{folder === 'INBOX' && unreadCount > 0 && ( {folder === 'INBOX' && unreadCount > 0 && (
<span className="ml-auto bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full text-[10px]"> <span className="inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded">
{unreadCount} {unreadCount}
</span> </span>
)} )}
@ -707,17 +710,7 @@ export default function CourrierPage() {
</Button> </Button>
)) ))
) : ( ) : (
<div className="px-2 py-2"> <div className="text-xs text-gray-500 px-2 py-1">No folders found</div>
<div className="flex flex-col space-y-2">
{/* Create placeholder folder items with shimmer effect */}
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="flex items-center gap-1.5 animate-pulse">
<div className="h-3.5 w-3.5 bg-gray-200 rounded-sm"></div>
<div className="h-3 w-24 bg-gray-200 rounded"></div>
</div>
))}
</div>
</div>
)} )}
</div> </div>
)} )}
@ -725,7 +718,27 @@ export default function CourrierPage() {
))} ))}
</div> </div>
)} )}
</div> </div>
{/* Categories Section */}
<div className="p-3 border-b border-gray-100">
<h4 className="mb-2 text-sm font-medium text-gray-500">Categories</h4>
<div className="space-y-1 pl-2">
<Button
variant="ghost"
className={`w-full justify-start text-sm py-1 px-2 ${
currentView === 'starred' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => handleMailboxChange('starred')}
>
<div className="flex gap-2 items-center">
<Star className="h-3.5 w-3.5" />
<span>Starred</span>
</div>
</Button>
</div>
</div>
</ScrollArea>
</div> </div>
{/* Email List and Content View */} {/* Email List and Content View */}