258 lines
8.0 KiB
TypeScript
258 lines
8.0 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { getMailboxes } from '@/lib/services/email-service';
|
|
import { getRedisClient } from '@/lib/redis';
|
|
import { getImapConnection } from '@/lib/services/email-service';
|
|
import { prisma } from '@/lib/prisma';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
// 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
|
|
const lastPrefetchMap = new Map<string, number>();
|
|
const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches
|
|
|
|
// Cache TTL for folders in Redis (5 minutes)
|
|
const FOLDERS_CACHE_TTL = 3600; // 1 hour
|
|
|
|
// Redis key for folders cache
|
|
const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders:${userId}:${accountId}`;
|
|
|
|
/**
|
|
* Ensure user exists in database, creating if missing
|
|
* Uses session data from Keycloak to populate user record
|
|
*/
|
|
async function ensureUserExists(session: any): Promise<void> {
|
|
const userId = session.user.id;
|
|
const userEmail = session.user.email;
|
|
|
|
if (!userId || !userEmail) {
|
|
throw new Error('Missing required user data in session');
|
|
}
|
|
|
|
try {
|
|
// Check if user exists
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: { id: userId }
|
|
});
|
|
|
|
if (existingUser) {
|
|
return;
|
|
}
|
|
|
|
// User doesn't exist, create it
|
|
console.log(`User ${userId} not found in database, creating from session data...`);
|
|
|
|
// Generate a temporary random password (not used for auth, Keycloak handles that)
|
|
const tempPassword = await bcrypt.hash(Math.random().toString(36).slice(-10), 10);
|
|
|
|
await prisma.user.create({
|
|
data: {
|
|
id: userId, // Use Keycloak user ID
|
|
email: userEmail,
|
|
password: tempPassword, // Temporary password (Keycloak handles authentication)
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}
|
|
});
|
|
|
|
console.log(`Successfully created user ${userId} (${userEmail}) in database`);
|
|
} catch (error) {
|
|
console.error(`Error ensuring user exists:`, error);
|
|
// If it's a unique constraint error, user might have been created by another request
|
|
if (error instanceof Error && error.message.includes('Unique constraint')) {
|
|
console.log('User may have been created by concurrent request, continuing...');
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This endpoint is called when the app initializes to check if the user has email credentials
|
|
* and to start prefetching email data in the background if they do
|
|
*/
|
|
export async function GET() {
|
|
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
|
|
console.log('Attempting to get server session...');
|
|
const session = await getServerSession(authOptions);
|
|
|
|
if (!session) {
|
|
console.error('No session found');
|
|
return NextResponse.json({
|
|
authenticated: false,
|
|
error: 'No session found'
|
|
}, { status: 401 });
|
|
}
|
|
|
|
if (!session.user) {
|
|
console.error('No user in session');
|
|
return NextResponse.json({
|
|
authenticated: false,
|
|
error: 'No user in session'
|
|
}, { status: 401 });
|
|
}
|
|
|
|
if (!session.user.id) {
|
|
console.error('No user ID in session');
|
|
return NextResponse.json({
|
|
authenticated: false,
|
|
error: 'No user ID in session'
|
|
}, { status: 401 });
|
|
}
|
|
|
|
console.log('Session validated successfully:', {
|
|
userId: session.user.id,
|
|
email: session.user.email,
|
|
name: session.user.name
|
|
});
|
|
|
|
// Ensure user exists in database (create if missing)
|
|
try {
|
|
await ensureUserExists(session);
|
|
} catch (error) {
|
|
console.error(`Error ensuring user exists:`, error);
|
|
return NextResponse.json({
|
|
authenticated: true,
|
|
hasEmailCredentials: false,
|
|
error: 'Failed to ensure user exists in database',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
});
|
|
}
|
|
|
|
// Get user with their accounts
|
|
console.log('Fetching user with ID:', session.user.id);
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: session.user.id },
|
|
include: { mailCredentials: true }
|
|
});
|
|
|
|
if (!user) {
|
|
console.error('User not found in database after creation attempt');
|
|
return NextResponse.json({
|
|
authenticated: true,
|
|
hasEmailCredentials: false,
|
|
error: 'User not found in database'
|
|
});
|
|
}
|
|
|
|
// Get all accounts for the user
|
|
const accounts = (user.mailCredentials || []) as MailCredentials[];
|
|
if (accounts.length === 0) {
|
|
console.log('No email accounts found for user:', session.user.id);
|
|
return NextResponse.json({
|
|
authenticated: true,
|
|
hasEmailCredentials: false,
|
|
accounts: [],
|
|
message: 'No email accounts found'
|
|
});
|
|
}
|
|
|
|
console.log(`Found ${accounts.length} accounts for user:`, accounts.map(a => a.email));
|
|
|
|
// Fetch folders for each account
|
|
const accountsWithFolders = await Promise.all(
|
|
accounts.map(async (account: MailCredentials) => {
|
|
const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id);
|
|
|
|
try {
|
|
// Try to get folders from Redis cache first
|
|
const cachedFolders = await redis.get(cacheKey);
|
|
if (cachedFolders) {
|
|
console.log(`Using cached folders for account ${account.email}`);
|
|
return {
|
|
id: account.id,
|
|
email: account.email,
|
|
display_name: account.display_name,
|
|
color: account.color,
|
|
folders: JSON.parse(cachedFolders)
|
|
};
|
|
}
|
|
|
|
// If not in cache, fetch from IMAP
|
|
console.log(`Fetching folders from IMAP for account ${account.email}`);
|
|
const client = await getImapConnection(user.id, account.id);
|
|
if (!client) {
|
|
console.warn(`Failed to get IMAP connection for account ${account.email}`);
|
|
return {
|
|
id: account.id,
|
|
email: account.email,
|
|
display_name: account.display_name,
|
|
color: account.color,
|
|
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
|
|
};
|
|
}
|
|
|
|
const folders = await getMailboxes(client);
|
|
console.log(`Fetched ${folders.length} folders for account ${account.email}`);
|
|
|
|
// Cache the folders in Redis
|
|
await redis.set(
|
|
cacheKey,
|
|
JSON.stringify(folders),
|
|
'EX',
|
|
FOLDERS_CACHE_TTL
|
|
);
|
|
|
|
return {
|
|
id: account.id,
|
|
email: account.email,
|
|
display_name: account.display_name,
|
|
color: account.color,
|
|
folders
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error fetching folders for account ${account.id}:`, error);
|
|
return {
|
|
id: account.id,
|
|
email: account.email,
|
|
display_name: account.display_name,
|
|
color: account.color,
|
|
folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
return NextResponse.json({
|
|
authenticated: true,
|
|
hasEmailCredentials: true,
|
|
allAccounts: accountsWithFolders
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in session route:', error);
|
|
return NextResponse.json(
|
|
{
|
|
authenticated: false,
|
|
error: 'Internal server error',
|
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|