Files
webmailserver/app/[lang]/dashboard/mailboxes/page.tsx

503 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useTransition, useCallback } from "react";
import { useSession } from "next-auth/react";
import { formatBytes } from "@/lib/format";
import { useDictionary } from "@/components/DictionaryContext";
interface Mailbox {
username: string;
name: string;
local_part: string;
domain: string;
quota: number;
quota_used: number;
active: string; // "1" | "0"
}
interface Domain {
domain_name: string;
}
interface User {
id: string;
name: string;
email: string;
telegramId?: string;
}
export default function MailboxesPage() {
const { data: session } = useSession();
const [domains, setDomains] = useState<Domain[]>([]);
const [selectedDomain, setSelectedDomain] = useState("");
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
const [loading, setLoading] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState<string | null>(null);
const [showInfoModal, setShowInfoModal] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const [search, setSearch] = useState("");
const [users, setUsers] = useState<User[]>([]);
const [createForm, setCreateForm] = useState({ local_part: "", name: "", password: "", quota: 3072, notifyUserId: "" });
const [newPassword, setNewPassword] = useState("");
const [formError, setFormError] = useState("");
const dict = useDictionary();
useEffect(() => {
// Fetch domains
fetch("/api/domains")
.then((r) => r.json())
.then((data: Domain[]) => {
if (Array.isArray(data) && data.length > 0) {
setDomains(data);
setSelectedDomain(data[0].domain_name);
}
});
// Fetch users for mapping selection
fetch("/api/users")
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data)) setUsers(data);
});
}, []);
const fetchMailboxes = useCallback(async (domain: string) => {
if (!domain) return;
setLoading(true);
const res = await fetch(`/api/mailboxes?domain=${encodeURIComponent(domain)}`);
const data = await res.json();
setMailboxes(Array.isArray(data) ? data : []);
setLoading(false);
}, []);
useEffect(() => {
if (selectedDomain) fetchMailboxes(selectedDomain);
}, [selectedDomain, fetchMailboxes]);
const handleCreate = (e: React.FormEvent) => {
e.preventDefault();
setFormError("");
startTransition(async () => {
try {
const res = await fetch("/api/mailboxes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...createForm, domain: selectedDomain }),
});
let data: any = {};
try {
data = await res.json();
} catch (e) {
data = { error: "Sunucudan geçersiz yanıt geldi (JSON hatası)." };
}
if (res.ok) {
// If a notification user is selected, create the mapping
if (createForm.notifyUserId) {
const fullEmail = `${createForm.local_part}@${selectedDomain}`;
await fetch("/api/mappings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: fullEmail,
userId: createForm.notifyUserId,
}),
});
}
setShowCreateModal(false);
setCreateForm({ local_part: "", name: "", password: "", quota: 3072, notifyUserId: "" });
fetchMailboxes(selectedDomain);
} else {
const msg = Array.isArray(data)
? data.map((d: { msg?: unknown }) => JSON.stringify(d.msg)).join(", ")
: (data?.error ?? "Mailcow bağlantısını veya veritabanını kontrol edin");
setFormError(String(msg));
}
} catch (error: any) {
console.error("Mailbox creation failed:", error);
setFormError(error.message);
}
});
};
const handleDelete = (username: string) => {
if (!confirm(`"${username}" ${dict.mailboxes.deleteConfirm || "hesabını silmek istediğinizden emin misiniz?"}`)) return;
startTransition(async () => {
await fetch(`/api/mailboxes/${encodeURIComponent(username)}`, { method: "DELETE" });
setMailboxes((prev) => prev.filter((m) => m.username !== username));
});
};
const handleToggle = (username: string, active: string) => {
const newActive = String(active) === "1" ? 0 : 1;
startTransition(async () => {
await fetch(`/api/mailboxes/${encodeURIComponent(username)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ active: newActive }),
});
setMailboxes((prev) =>
prev.map((m) => m.username === username ? { ...m, active: String(newActive) } : m)
);
});
};
const handlePasswordChange = (e: React.FormEvent) => {
e.preventDefault();
if (!showPasswordModal) return;
startTransition(async () => {
await fetch(`/api/mailboxes/${encodeURIComponent(showPasswordModal)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: newPassword }),
});
setShowPasswordModal(null);
setNewPassword("");
});
};
const filtered = mailboxes.filter(
(m) =>
m.username.toLowerCase().includes(search.toLowerCase()) ||
m.name.toLowerCase().includes(search.toLowerCase())
);
const isSuperAdmin = session?.user?.role === "SUPER_ADMIN";
return (
<>
<div className="page-header">
<div>
<h1 className="page-title">{dict.mailboxes.title || "Mail Hesapları"}</h1>
<p className="page-subtitle">
{selectedDomain
? `${selectedDomain}${mailboxes.length} ${dict.mailboxes.accounts || "hesap"}`
: (dict.mailboxes.selectDomain || "Domain seçin")}
</p>
</div>
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
<select
className="input"
style={{ width: "auto", minWidth: 200 }}
value={selectedDomain}
onChange={(e) => setSelectedDomain(e.target.value)}
>
{domains.map((d) => (
<option key={d.domain_name} value={d.domain_name}>
{d.domain_name}
</option>
))}
</select>
<button
className="btn btn-ghost"
onClick={() => fetchMailboxes(selectedDomain)}
disabled={!selectedDomain}
>
<RefreshIcon />
</button>
<button
className="btn btn-primary"
onClick={() => setShowCreateModal(true)}
disabled={!selectedDomain}
>
<PlusIcon /> {dict.mailboxes.addAccount || "Hesap Ekle"}
</button>
</div>
</div>
<div className="page-body">
<div className="search-bar">
<div className="search-input-wrap">
<span className="search-icon"><SearchIcon /></span>
<input
type="text"
className="input search-input"
placeholder={dict.mailboxes.searchPlaceholder || "E-posta veya isim ara..."}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="table-wrap">
{loading ? (
<div className="empty-state">
<span className="spinner" style={{ width: 24, height: 24 }} />
</div>
) : filtered.length === 0 ? (
<div className="empty-state">
<div className="empty-icon"><MailIcon size={24} /></div>
<div style={{ fontWeight: 600 }}>
{selectedDomain ? (dict.mailboxes.noMailboxes || "Bu domainde mail hesabı yok") : (dict.mailboxes.selectDomain || "Domain seçin")}
</div>
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>
{selectedDomain ? (dict.mailboxes.noMailboxesDesc || "\"Hesap Ekle\" butonuna tıklayın") : (dict.mailboxes.selectDomainDesc || "Sol üstteki listeden domain seçin")}
</div>
</div>
) : (
<table>
<thead>
<tr>
<th>{dict.mailboxes.email || "E-posta"}</th>
<th>{dict.mailboxes.name || "Ad Soyad"}</th>
<th>{dict.mailboxes.quota || "Kota"}</th>
<th>{dict.mailboxes.status || "Durum"}</th>
<th>{dict.mailboxes.actions || "İşlemler"}</th>
</tr>
</thead>
<tbody>
{filtered.map((m) => {
const usedPct = m.quota > 0 ? Math.min((m.quota_used / m.quota) * 100, 100) : 0;
return (
<tr key={m.username}>
<td>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div style={{
width: 30, height: 30, borderRadius: 6,
background: "var(--accent-dim)", color: "var(--accent-hover)",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0, fontSize: 13, fontWeight: 700,
}}>
{m.local_part[0]?.toUpperCase()}
</div>
<span style={{ fontWeight: 500 }}>{m.username}</span>
</div>
</td>
<td style={{ color: "var(--text-secondary)" }}>{m.name}</td>
<td style={{ minWidth: 160 }}>
<div style={{ fontSize: 11, color: "var(--text-secondary)", marginBottom: 4 }}>
{formatBytes(m.quota_used)} / {formatBytes(m.quota)}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div className="progress-bar">
<div className={`progress-fill ${usedPct > 80 ? "danger" : ""}`} style={{ width: `${usedPct}%` }} />
</div>
<span style={{ fontSize: 11, color: "var(--text-muted)", flexShrink: 0 }}>{Math.round(usedPct)}%</span>
</div>
</td>
<td>
<span className={`badge ${String(m.active) === "1" ? "badge-green" : "badge-red"}`}>
{String(m.active) === "1" ? `${dict.mailboxes.active || "Aktif"}` : `${dict.mailboxes.inactive || "Pasif"}`}
</span>
</td>
<td>
<div style={{ display: "flex", gap: 6 }}>
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowInfoModal(m.username)}
title={dict.mailboxes.info || "Bağlantı Bilgileri"}
>
<InfoIcon />
</button>
<button
className="btn btn-ghost btn-sm"
onClick={() => setShowPasswordModal(m.username)}
title={dict.mailboxes.changePassword || "Şifre Değiştir"}
>
<KeyIcon />
</button>
<button
className={`btn btn-sm ${String(m.active) === "1" ? "btn-ghost" : "btn-success"}`}
onClick={() => handleToggle(m.username, m.active)}
title={String(m.active) === "1" ? (dict.mailboxes.deactivate || "Pasife Al") : (dict.mailboxes.activate || "Aktif Et")}
>
{String(m.active) === "1" ? <PauseIcon /> : <PlayIcon />}
</button>
<button
className="btn btn-danger btn-sm"
onClick={() => handleDelete(m.username)}
title={dict.mailboxes.delete || "Sil"}
>
<TrashIcon />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
{/* Create Modal */}
{showCreateModal && (
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowCreateModal(false)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">{dict.mailboxes.newAccount || "Yeni Mail Hesabı"}</h2>
<button className="modal-close" onClick={() => setShowCreateModal(false)}><XIcon /></button>
</div>
<form onSubmit={handleCreate}>
<div className="modal-body form-group">
{formError && <div className="error-msg">{formError}</div>}
<div>
<label className="label">{dict.mailboxes.username || "Kullanıcı Adı"}</label>
<div style={{ display: "flex" }}>
<input
type="text" className="input"
style={{ borderRadius: "var(--radius) 0 0 var(--radius)" }}
placeholder="info"
value={createForm.local_part}
onChange={(e) => setCreateForm({ ...createForm, local_part: e.target.value })}
required
/>
<span style={{
background: "var(--bg-hover)", border: "1px solid var(--border)", borderLeft: "none",
padding: "8px 12px", borderRadius: "0 var(--radius) var(--radius) 0",
color: "var(--text-secondary)", fontSize: 13, whiteSpace: "nowrap",
}}>
@{selectedDomain}
</span>
</div>
</div>
<div>
<label className="label">{dict.mailboxes.name || "Ad Soyad"}</label>
<input type="text" className="input" placeholder="Emina Karabudak"
value={createForm.name}
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
required />
</div>
<div>
<label className="label">{dict.mailboxes.password || "Şifre"}</label>
<input type="password" className="input" placeholder="********"
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
required />
</div>
<div>
<label className="label">{dict.mailboxes.quotaMb || "Kota (MB)"}</label>
<input type="number" className="input" value={createForm.quota} min={100} max={102400}
onChange={(e) => setCreateForm({ ...createForm, quota: parseInt(e.target.value) || 3072 })} />
</div>
<div>
<label className="label">{dict.mailboxes.notifyUser || "Bildirim Gidecek Kullanıcı (TG)"}</label>
<select
className="input"
value={createForm.notifyUserId}
onChange={(e) => setCreateForm({ ...createForm, notifyUserId: e.target.value })}
>
<option value="">-- {dict.mailboxes.noNotify || "Bildirim Gönderme"} --</option>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.name || u.email} {u.telegramId ? `(TG: ${u.telegramId})` : ""}
</option>
))}
</select>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-ghost" onClick={() => setShowCreateModal(false)}>{dict.mailboxes.cancel || "İptal"}</button>
<button type="submit" className="btn btn-primary" disabled={isPending}>
{isPending ? <span className="spinner" /> : <PlusIcon />} {dict.mailboxes.create || "Oluştur"}
</button>
</div>
</form>
</div>
</div>
)}
{/* Password Modal */}
{showPasswordModal && (
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowPasswordModal(null)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">{dict.mailboxes.changePassword || "Şifre Değiştir"}</h2>
<button className="modal-close" onClick={() => setShowPasswordModal(null)}><XIcon /></button>
</div>
<form onSubmit={handlePasswordChange}>
<div className="modal-body form-group">
<div style={{ fontSize: 13, color: "var(--text-secondary)" }}>
<strong style={{ color: "var(--text-primary)" }}>{showPasswordModal}</strong> {dict.mailboxes.newPasswordFor || "için yeni şifre"}
</div>
<div>
<label className="label">{dict.mailboxes.password || "Yeni Şifre"}</label>
<input type="password" className="input" placeholder="********"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required autoFocus />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-ghost" onClick={() => setShowPasswordModal(null)}>{dict.mailboxes.cancel || "İptal"}</button>
<button type="submit" className="btn btn-primary" disabled={isPending}>
{isPending ? <span className="spinner" /> : <KeyIcon />} {dict.mailboxes.update || "Güncelle"}
</button>
</div>
</form>
</div>
</div>
)}
{/* Info Modal */}
{showInfoModal && (
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowInfoModal(null)}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">{dict.mailboxes.connectionInfo || "İstemci Bağlantı Bilgileri"}</h2>
<button className="modal-close" onClick={() => setShowInfoModal(null)}><XIcon /></button>
</div>
<div className="modal-body form-group">
<div style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 10 }}>
<strong>{showInfoModal}</strong> {dict.mailboxes.connectionInfoDesc || "hesabını kurmak için aşağıdaki bilgileri kullanın:"}
</div>
<div style={{ background: "var(--bg-hover)", padding: 12, borderRadius: "var(--radius)", border: "1px solid var(--border)", fontSize: 13 }}>
<div style={{ marginBottom: 12 }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.imap || "IMAP (Gelen Sunucu)"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>{dict.mailboxes.server || "Sunucu"}: <strong>mail.ayris.tech</strong></span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
</div>
<div style={{ marginTop: 2 }}>{dict.mailboxes.port || "Port"}: <strong>993</strong> (SSL/TLS)</div>
</div>
<div style={{ marginBottom: 12, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.smtp || "SMTP (Giden Sunucu)"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>{dict.mailboxes.server || "Sunucu"}: <strong>mail.ayris.tech</strong></span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText("mail.ayris.tech")} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
</div>
<div style={{ marginTop: 2 }}>{dict.mailboxes.port || "Port"}: <strong>587</strong> (STARTTLS) <span style={{ color: "var(--text-secondary)" }}>veya 465 (SSL)</span></div>
</div>
<div style={{ paddingTop: 12, borderTop: "1px solid var(--border)" }}>
<div style={{ color: "var(--text-secondary)", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.5 }}>{dict.mailboxes.auth || "Kimlik Doğrulama"}</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4, alignItems: "center" }}>
<span>{dict.mailboxes.username || "Kullanıcı Adı"}: <strong>{showInfoModal}</strong></span>
<button className="btn btn-ghost btn-sm" style={{ padding: "4px 8px", fontSize: 11 }} onClick={() => navigator.clipboard.writeText(showInfoModal)} title="Kopyala"><CopyIcon /> {dict.mailboxes.copy || "Kopyala"}</button>
</div>
<div style={{ color: "var(--text-secondary)", marginTop: 4 }}>{dict.mailboxes.password || "Şifre"}: <span style={{ fontStyle: "italic" }}>{dict.mailboxes.authPassword || "Hesap oluştururken belirlediğiniz şifre"}</span></div>
</div>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-primary" onClick={() => setShowInfoModal(null)}>{dict.mailboxes.ok || "Tamam"}</button>
</div>
</div>
</div>
)}
</>
);
}
// Icons
function PlusIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>; }
function SearchIcon() { return <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>; }
function RefreshIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>; }
function MailIcon({ size = 13 }: { size?: number }) { return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>; }
function TrashIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>; }
function KeyIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>; }
function PauseIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>; }
function PlayIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>; }
function XIcon() { return <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>; }
function InfoIcon() { return <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>; }
function CopyIcon() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>; }