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 { 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}`);
|
||||||
|
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user