courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 14:21:41 +02:00
parent 9f5617b424
commit 093125df23
3 changed files with 164 additions and 77 deletions

View File

@ -7,7 +7,12 @@ import {
testEmailConnection testEmailConnection
} from '@/lib/services/email-service'; } from '@/lib/services/email-service';
import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; import { prefetchUserEmailData } from '@/lib/services/prefetch-service';
import { cacheEmailCredentials, invalidateUserEmailCache } from '@/lib/redis'; import {
cacheEmailCredentials,
invalidateUserEmailCache,
getCachedEmailCredentials
} from '@/lib/redis';
import { prisma } from '@/lib/prisma';
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
@ -49,13 +54,18 @@ export async function POST(request: Request) {
// Invalidate all cached data for this user as they are changing their credentials // Invalidate all cached data for this user as they are changing their credentials
await invalidateUserEmailCache(session.user.id); await invalidateUserEmailCache(session.user.id);
// Save credentials in the database and Redis // Create credentials object with required fields
await saveUserEmailCredentials(session.user.id, email, { const credentials = {
email, email,
password, password,
host, host,
port: parseInt(port) port: parseInt(port),
}); secure: true // Default to secure connection
};
// Save credentials in the database and Redis
// Use email as the accountId since it's unique per user
await saveUserEmailCredentials(session.user.id, email, credentials);
// Start prefetching email data in the background // Start prefetching email data in the background
// We don't await this to avoid blocking the response // We don't await this to avoid blocking the response
@ -67,10 +77,7 @@ export async function POST(request: Request) {
} catch (error) { } catch (error) {
console.error('Error in login handler:', error); console.error('Error in login handler:', error);
return NextResponse.json( return NextResponse.json(
{ { error: 'An unexpected error occurred' },
error: 'An unexpected error occurred',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 } { status: 500 }
); );
} }
@ -78,7 +85,6 @@ export async function POST(request: Request) {
export async function GET() { export async function GET() {
try { try {
// Authenticate user
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json( return NextResponse.json(
@ -87,8 +93,26 @@ export async function GET() {
); );
} }
// Get user credentials from database // First try to get from Redis cache
const credentials = await getUserEmailCredentials(session.user.id); let credentials = await getCachedEmailCredentials(session.user.id, 'default');
// If not in cache, get from database
if (!credentials) {
credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
},
select: {
email: true,
host: true,
port: true
}
});
} else {
// Remove password from response
const { password, ...safeCredentials } = credentials;
credentials = safeCredentials;
}
if (!credentials) { if (!credentials) {
return NextResponse.json( return NextResponse.json(
@ -97,14 +121,8 @@ export async function GET() {
); );
} }
// Return credentials without the password return NextResponse.json(credentials);
return NextResponse.json({
email: credentials.email,
host: credentials.host,
port: credentials.port
});
} catch (error) { } catch (error) {
console.error('Error fetching credentials:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to retrieve credentials' }, { error: 'Failed to retrieve credentials' },
{ status: 500 } { status: 500 }

View File

@ -4,9 +4,26 @@ import { authOptions } from '@/lib/auth';
import { getMailboxes } from '@/lib/services/email-service'; import { getMailboxes } from '@/lib/services/email-service';
import { getRedisClient } from '@/lib/redis'; import { getRedisClient } from '@/lib/redis';
import { getImapConnection } from '@/lib/services/email-service'; import { getImapConnection } from '@/lib/services/email-service';
import { MailCredentials } from '@prisma/client';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
// Define extended MailCredentials type
interface MailCredentials {
id: string;
userId: string;
email: string;
password: string;
host: string;
port: number;
secure?: boolean;
smtp_host?: string | null;
smtp_port?: number | null;
smtp_secure?: boolean | null;
display_name?: string | null;
color?: string | null;
createdAt: Date;
updatedAt: Date;
}
// Keep track of last prefetch time for each user // Keep track of last prefetch time for each user
const lastPrefetchMap = new Map<string, number>(); const lastPrefetchMap = new Map<string, number>();
const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches
@ -23,23 +40,39 @@ const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders:
*/ */
export async function GET() { export async function GET() {
try { try {
// Get Redis connection first to ensure it's available
const redis = getRedisClient();
if (!redis) {
console.error('Redis connection failed');
return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 });
}
// Get session with detailed logging // Get session with detailed logging
console.log('Attempting to get server session...'); console.log('Attempting to get server session...');
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session) { if (!session) {
console.error('No session found'); console.error('No session found');
return NextResponse.json({ error: 'No session found' }, { status: 401 }); return NextResponse.json({
authenticated: false,
error: 'No session found'
}, { status: 401 });
} }
if (!session.user) { if (!session.user) {
console.error('No user in session'); console.error('No user in session');
return NextResponse.json({ error: 'No user in session' }, { status: 401 }); return NextResponse.json({
authenticated: false,
error: 'No user in session'
}, { status: 401 });
} }
if (!session.user.id) { if (!session.user.id) {
console.error('No user ID in session'); console.error('No user ID in session');
return NextResponse.json({ error: 'No user ID in session' }, { status: 401 }); return NextResponse.json({
authenticated: false,
error: 'No user ID in session'
}, { status: 401 });
} }
console.log('Session validated successfully:', { console.log('Session validated successfully:', {
@ -48,13 +81,6 @@ export async function GET() {
name: session.user.name name: session.user.name
}); });
// Get Redis connection
const redis = getRedisClient();
if (!redis) {
console.error('Redis connection failed');
return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 });
}
// Get user with their accounts // Get user with their accounts
console.log('Fetching user with ID:', session.user.id); console.log('Fetching user with ID:', session.user.id);
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@ -64,7 +90,11 @@ export async function GET() {
if (!user) { if (!user) {
console.error('User not found in database'); console.error('User not found in database');
return NextResponse.json({ error: 'User not found' }, { status: 404 }); return NextResponse.json({
authenticated: true,
hasEmailCredentials: false,
error: 'User not found in database'
});
} }
// Get all accounts for the user // Get all accounts for the user
@ -73,6 +103,7 @@ export async function GET() {
console.log('No email accounts found for user:', session.user.id); console.log('No email accounts found for user:', session.user.id);
return NextResponse.json({ return NextResponse.json({
authenticated: true, authenticated: true,
hasEmailCredentials: false,
accounts: [], accounts: [],
message: 'No email accounts found' message: 'No email accounts found'
}); });
@ -84,12 +115,17 @@ export async function GET() {
const accountsWithFolders = await Promise.all( const accountsWithFolders = await Promise.all(
accounts.map(async (account: MailCredentials) => { accounts.map(async (account: MailCredentials) => {
const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id); const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id);
try {
// Try to get folders from Redis cache first // Try to get folders from Redis cache first
const cachedFolders = await redis.get(cacheKey); const cachedFolders = await redis.get(cacheKey);
if (cachedFolders) { if (cachedFolders) {
console.log(`Using cached folders for account ${account.email}`); console.log(`Using cached folders for account ${account.email}`);
return { return {
...account, id: account.id,
email: account.email,
display_name: account.display_name,
color: account.color,
folders: JSON.parse(cachedFolders) folders: JSON.parse(cachedFolders)
}; };
} }
@ -100,14 +136,17 @@ export async function GET() {
if (!client) { if (!client) {
console.warn(`Failed to get IMAP connection for account ${account.email}`); console.warn(`Failed to get IMAP connection for account ${account.email}`);
return { return {
...account, id: account.id,
email: account.email,
display_name: account.display_name,
color: account.color,
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
}; };
} }
try {
const folders = await getMailboxes(client); const folders = await getMailboxes(client);
console.log(`Fetched ${folders.length} folders for account ${account.email}`); console.log(`Fetched ${folders.length} folders for account ${account.email}`);
// Cache the folders in Redis // Cache the folders in Redis
await redis.set( await redis.set(
cacheKey, cacheKey,
@ -115,14 +154,21 @@ export async function GET() {
'EX', 'EX',
FOLDERS_CACHE_TTL FOLDERS_CACHE_TTL
); );
return { return {
...account, id: account.id,
email: account.email,
display_name: account.display_name,
color: account.color,
folders folders
}; };
} catch (error) { } catch (error) {
console.error(`Error fetching folders for account ${account.id}:`, error); console.error(`Error fetching folders for account ${account.id}:`, error);
return { return {
...account, id: account.id,
email: account.email,
display_name: account.display_name,
color: account.color,
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
}; };
} }
@ -131,12 +177,17 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
authenticated: true, authenticated: true,
hasEmailCredentials: true,
accounts: accountsWithFolders accounts: accountsWithFolders
}); });
} catch (error) { } catch (error) {
console.error('Error in session route:', error); console.error('Error in session route:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' }, {
authenticated: false,
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 } { status: 500 }
); );
} }

View File

@ -156,7 +156,12 @@ export async function getImapConnection(
*/ */
export async function getUserEmailCredentials(userId: string, accountId?: string): Promise<EmailCredentials | null> { export async function getUserEmailCredentials(userId: string, accountId?: string): Promise<EmailCredentials | null> {
const credentials = await prisma.mailCredentials.findFirst({ const credentials = await prisma.mailCredentials.findFirst({
where: accountId ? { userId, id: accountId } : { userId } where: {
AND: [
{ userId },
{ email: accountId }
]
}
}); });
if (!credentials) return null; if (!credentials) return null;
@ -197,7 +202,6 @@ export async function saveUserEmailCredentials(
credentials: EmailCredentials credentials: EmailCredentials
): Promise<void> { ): Promise<void> {
console.log('Saving credentials for user:', userId, 'account:', accountId); console.log('Saving credentials for user:', userId, 'account:', accountId);
console.log('Saving credentials for user:', userId, 'account:', credentials);
if (!credentials) { if (!credentials) {
throw new Error('No credentials provided'); throw new Error('No credentials provided');
@ -217,22 +221,34 @@ export async function saveUserEmailCredentials(
color: credentials.color || null color: credentials.color || null
}; };
// Save to database try {
// Save to database using the unique constraint on [userId, email]
await prisma.mailCredentials.upsert({ await prisma.mailCredentials.upsert({
where: { where: {
id: accountId, id: await prisma.mailCredentials.findFirst({
userId where: {
AND: [
{ userId },
{ email: accountId }
]
},
select: { id: true }
}).then(result => result?.id ?? '')
}, },
update: dbCredentials, update: dbCredentials,
create: { create: {
id: accountId,
userId, userId,
...dbCredentials ...dbCredentials
} }
}); });
// Cache the full credentials object in Redis (with all fields) // Cache the full credentials object in Redis
await cacheEmailCredentials(userId, accountId, credentials); await cacheEmailCredentials(userId, accountId, credentials);
console.log('Successfully saved and cached credentials for user:', userId);
} catch (error) {
console.error('Error saving credentials:', error);
throw error;
}
} }
// Helper type for IMAP fetch options // Helper type for IMAP fetch options
@ -274,10 +290,12 @@ export async function getEmails(
if (accountId) { if (accountId) {
try { try {
// Get account from database // Get account from database
const account = await prisma.mailCredentials.findUnique({ const account = await prisma.mailCredentials.findFirst({
where: { where: {
id: accountId, AND: [
userId { userId },
{ email: accountId }
]
} }
}); });