courrier multi account
This commit is contained in:
parent
b66f1adb3a
commit
9f9507b784
133
app/api/admin/restore-credentials/route.ts
Normal file
133
app/api/admin/restore-credentials/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
65
app/api/admin/view-redis-credentials/route.ts
Normal file
65
app/api/admin/view-redis-credentials/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<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) {
|
||||
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}`);
|
||||
|
||||
|
||||
@ -428,26 +428,28 @@ export default function CourrierPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Accounts Section */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between text-sm font-medium text-gray-500"
|
||||
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
||||
>
|
||||
<span>Accounts</span>
|
||||
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setShowAddAccountForm(!showAddAccountForm)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Scrollable area for accounts and folders */}
|
||||
<ScrollArea className="flex-1 h-0">
|
||||
{/* Accounts Section */}
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between text-sm font-medium text-gray-500"
|
||||
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
||||
>
|
||||
<span>Accounts</span>
|
||||
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setShowAddAccountForm(!showAddAccountForm)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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() {
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
||||
<span className="font-medium text-gray-700 truncate">{account.name}</span>
|
||||
</div>
|
||||
{account.id !== 0 && (
|
||||
<div className="flex items-center">
|
||||
{showFolders ?
|
||||
<ChevronDown className="h-3.5 w-3.5 text-gray-500" /> :
|
||||
<ChevronRight className="h-3.5 w-3.5 text-gray-500" />
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{/* Show arrow for all accounts */}
|
||||
<div className="flex items-center">
|
||||
{selectedAccount?.id === account.id && showFolders ?
|
||||
<ChevronDown className="h-3.5 w-3.5 text-gray-500" /> :
|
||||
<ChevronRight className="h-3.5 w-3.5 text-gray-500" />
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{/* 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 && (
|
||||
<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.map((folder) => (
|
||||
@ -699,7 +702,7 @@ export default function CourrierPage() {
|
||||
<span className="truncate">{formatFolderName(folder)}</span>
|
||||
</div>
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@ -707,17 +710,7 @@ export default function CourrierPage() {
|
||||
</Button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-2 py-2">
|
||||
<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 className="text-xs text-gray-500 px-2 py-1">No folders found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -725,7 +718,27 @@ export default function CourrierPage() {
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* Email List and Content View */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user