courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 18:18:37 +02:00
parent c734f52444
commit d3ff8e6b19
2 changed files with 154 additions and 10 deletions

View File

@ -156,4 +156,85 @@ export async function POST(request: Request) {
{ status: 500 }
);
}
}
}
export async function DELETE(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const accountId = searchParams.get('accountId');
if (!accountId) {
return NextResponse.json({ error: 'Missing accountId' }, { status: 400 });
}
// Find the account
const account = await prisma.mailCredentials.findFirst({
where: { id: accountId, userId: session.user.id },
});
if (!account) {
return NextResponse.json({ error: 'Account not found' }, { status: 404 });
}
// Delete from database
await prisma.mailCredentials.delete({ where: { id: accountId } });
// Invalidate cache
await invalidateFolderCache(session.user.id, account.email, '*');
return NextResponse.json({ success: true, message: 'Account deleted' });
} catch (error) {
console.error('Error deleting account:', error);
return NextResponse.json({ error: 'Failed to delete account', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}
export async function PATCH(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { accountId, newPassword } = body;
if (!accountId || !newPassword) {
return NextResponse.json({ error: 'Missing accountId or newPassword' }, { status: 400 });
}
// Find the account
const account = await prisma.mailCredentials.findFirst({
where: { id: accountId, userId: session.user.id },
});
if (!account) {
return NextResponse.json({ error: 'Account not found' }, { status: 404 });
}
// Test new credentials
const testResult = await testEmailConnection({
email: account.email,
password: newPassword,
host: account.host,
port: account.port,
secure: typeof (account as any).secure === 'boolean' ? (account as any).secure : true,
});
if (!testResult.imap) {
return NextResponse.json({ error: `Connection test failed: ${testResult.error || 'Could not connect to IMAP server'}` }, { status: 400 });
}
// Update password in database
await prisma.mailCredentials.update({
where: { id: accountId },
data: { password: newPassword },
});
// Invalidate cache
await invalidateFolderCache(session.user.id, account.email, '*');
return NextResponse.json({ success: true, message: 'Password updated' });
} catch (error) {
console.error('Error updating account password:', error);
return NextResponse.json({ error: 'Failed to update password', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
}
}
const handleAddAccount = async (accountData: AccountData) => {
// ... account creation logic ...
// setAccounts(prev => [...prev, newAccount]);
// setVisibleFolders(prev => ({
// ...prev,
// [newAccount.id]: newAccount.folders
// }));
};

View File

@ -31,6 +31,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { toast } from '@/components/ui/use-toast';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
// Import components
import EmailSidebar from '@/components/email/EmailSidebar';
@ -163,6 +164,15 @@ export default function CourrierPage() {
// Track folder visibility per account
const [visibleFolders, setVisibleFolders] = useState<Record<string, string[]>>({});
// Add state for modals/dialogs
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [accountToEdit, setAccountToEdit] = useState<Account | null>(null);
const [accountToDelete, setAccountToDelete] = useState<Account | null>(null);
const [newPassword, setNewPassword] = useState('');
const [editLoading, setEditLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
// Debug accounts state
useEffect(() => {
console.log('Current accounts state:', accounts);
@ -1024,17 +1034,24 @@ export default function CourrierPage() {
<div className="flex items-center w-full">
<div className={`w-3 h-3 rounded-full ${account.color?.startsWith('#') ? 'bg-blue-500' : account.color || 'bg-blue-500'} mr-2`}></div>
<span className="truncate text-gray-700">{account.name}</span>
{/* More options button */}
{account.id !== 'all-accounts' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="ml-1 h-5 w-5 p-0 text-gray-400 hover:text-gray-600" onClick={e => e.stopPropagation()}>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={e => { e.stopPropagation(); setAccountToEdit(account); setShowEditModal(true); }}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={e => { e.stopPropagation(); setAccountToDelete(account); setShowDeleteDialog(true); }}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{account.id !== 'all-accounts' && (
<span
className="ml-auto text-gray-400 hover:text-gray-600 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
// Toggle this specific account's expanded state
setExpandedAccounts(prev => ({
...prev,
[account.id]: !prev[account.id]
}));
}}
className="ml-1 text-gray-400 hover:text-gray-600 cursor-pointer"
onClick={e => { e.stopPropagation(); setExpandedAccounts(prev => ({ ...prev, [account.id]: !prev[account.id] })); }}
>
{expandedAccounts[account.id] ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</span>
@ -1259,6 +1276,52 @@ export default function CourrierPage() {
/>
</DialogContent>
</Dialog>
{/* Edit Password Modal */}
<Dialog open={showEditModal} onOpenChange={open => { if (!open) setShowEditModal(false); }}>
<DialogContent className="sm:max-w-[400px]">
<DialogTitle>Edit Account Password</DialogTitle>
<form onSubmit={async e => {
e.preventDefault();
if (!accountToEdit) return;
setEditLoading(true);
try {
const res = await fetch('/api/courrier/account', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId: accountToEdit.id, newPassword }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to update password');
toast({ title: 'Password updated', description: 'Password changed successfully.' });
setShowEditModal(false);
setNewPassword('');
// Refresh accounts (re-fetch or reload page)
window.location.reload();
} catch (err) {
toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update password', variant: 'destructive' });
} finally {
setEditLoading(false);
}
}}>
<div className="mb-2">
<Label htmlFor="new-password">New Password</Label>
<Input id="new-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required className="mt-1" />
</div>
<div className="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)}>Cancel</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* Delete Account Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={open => { if (!open) setShowDeleteDialog(false); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Account</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this account? This action cannot be undone.
</>
);
}