diff --git a/app/[lang]/dashboard/domains/page.tsx b/app/[lang]/dashboard/domains/page.tsx index b22ba4e..9dd1517 100644 --- a/app/[lang]/dashboard/domains/page.tsx +++ b/app/[lang]/dashboard/domains/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useTransition } from "react"; import { formatBytes } from "@/lib/format"; +import { useDictionary } from "@/components/DictionaryContext"; interface Domain { domain_name: string; @@ -23,6 +24,7 @@ export default function DomainsPage() { const [search, setSearch] = useState(""); const [form, setForm] = useState({ domain: "", description: "", mailboxes: "10", quota: "10240", maxquota: "10240" }); const [formError, setFormError] = useState(""); + const dict = useDictionary(); const fetchDomains = async () => { setLoading(true); @@ -54,14 +56,23 @@ export default function DomainsPage() { fetchDomains(); } else { const data = await res.json(); - const msg = Array.isArray(data) ? data.map((d: { msg?: string }) => d.msg).join(", ") : (data?.error ?? "Bir hata oluştu"); - setFormError(String(msg)); + let msg = "Bir hata oluştu"; + if (Array.isArray(data)) { + msg = data.map((d: any) => { + if (typeof d.msg === "string") return d.msg; + if (Array.isArray(d.msg)) return d.msg.join(", "); + return JSON.stringify(d.msg || d); + }).join(" | "); + } else if (data?.error) { + msg = data.error; + } + setFormError(msg); } }); }; const handleDelete = (domain: string) => { - if (!confirm(`"${domain}" domainini Mailcow'dan silmek istediğinizden emin misiniz?\n\nBu işlem geri alınamaz!`)) return; + if (!confirm(`"${domain}" domainini silmek istediğinizden emin misiniz?\n\nBu işlem geri alınamaz!`)) return; startTransition(async () => { await fetch(`/api/domains/${encodeURIComponent(domain)}`, { method: "DELETE" }); fetchDomains(); @@ -78,11 +89,11 @@ export default function DomainsPage() { <>
-

Domainler

-

Mailcow üzerindeki tüm domainleri yönetin

+

{dict.domains.title || "Domainler"}

+

{domains.length} {dict.domains.subtitle || "domain listeleniyor"}

@@ -93,13 +104,13 @@ export default function DomainsPage() { setSearch(e.target.value)} /> @@ -109,19 +120,19 @@ export default function DomainsPage() { ) : filtered.length === 0 ? (
-
Domain bulunamadı
-
Mailcow'a domain eklemek için "Domain Ekle" butonuna tıklayın.
+
{dict.domains.noDomains || "Domain bulunamadı"}
+
{dict.domains.tryDiffSearch || "Farklı bir arama yapın."}
) : ( - - - - - - + + + + + + @@ -160,7 +171,7 @@ export default function DomainsPage() {
DomainMail KutularıAliasKotaDurumİşlemler{dict.domains.domain || "Domain"}{dict.domains.mailboxes || "Mail Kutuları"}{dict.domains.aliases || "Alias"}{dict.domains.quota || "Kota"}{dict.domains.status || "Durum"}{dict.domains.actions || "İşlemler"}
- {String(d.active) === "1" ? "● Aktif" : "● Pasif"} + {String(d.active) === "1" ? `● ${dict.domains.active || "Aktif"}` : `● ${dict.domains.inactive || "Pasif"}`} @@ -181,20 +192,20 @@ export default function DomainsPage() {
e.target === e.currentTarget && setShowModal(false)}>
-

Mailcow'a Domain Ekle

+

{dict.domains.addDomain || "Domain Ekle"}

{formError &&
{formError}
}
- + setForm({ ...form, domain: e.target.value })} required />
- - {dict.domains.description || "Açıklama"} + setForm({ ...form, description: e.target.value })} />
diff --git a/app/[lang]/dashboard/layout.tsx b/app/[lang]/dashboard/layout.tsx index 2a8f11e..98e1cec 100644 --- a/app/[lang]/dashboard/layout.tsx +++ b/app/[lang]/dashboard/layout.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import Providers from "@/components/Providers"; import Sidebar from "@/components/Sidebar"; import { getDictionary, Locale } from "@/app/dictionaries"; +import { DictionaryProvider } from "@/components/DictionaryContext"; export default async function DashboardLayout( props: { @@ -23,10 +24,12 @@ export default async function DashboardLayout( return ( -
- -
{children}
-
+ +
+ +
{children}
+
+
); } diff --git a/app/[lang]/dashboard/mail/page.tsx b/app/[lang]/dashboard/mail/page.tsx index 31274d5..486007e 100644 --- a/app/[lang]/dashboard/mail/page.tsx +++ b/app/[lang]/dashboard/mail/page.tsx @@ -1,11 +1,13 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import { useParams } from "next/navigation"; import MailLogin from "@/components/mail/MailLogin"; import FolderList from "@/components/mail/FolderList"; import MessageList from "@/components/mail/MessageList"; import MessageView from "@/components/mail/MessageView"; import ComposeModal from "@/components/mail/ComposeModal"; +import { useDictionary } from "@/components/DictionaryContext"; export interface MailFolder { name: string; @@ -34,6 +36,8 @@ export interface MailMessage extends MailEnvelope { } export default function MailPage() { + const params = useParams(); + const lang = params.lang as string; const [connected, setConnected] = useState(null); const [email, setEmail] = useState(""); const [folders, setFolders] = useState([]); @@ -44,6 +48,20 @@ export default function MailPage() { const [loading, setLoading] = useState(false); const [showCompose, setShowCompose] = useState(false); const [replyTo, setReplyTo] = useState(null); + const dict = useDictionary(); + + const getFolderLabel = (path: string): string => { + const folder = folders.find((f) => f.path === path); + const name = folder?.name || path; + const lower = name.toLowerCase(); + if (lower === "inbox") return dict.mailClient.inbox || "Inbox"; + if (lower === "sent") return dict.mailClient.sent || "Sent"; + if (lower === "drafts") return dict.mailClient.drafts || "Drafts"; + if (lower === "trash") return dict.mailClient.trash || "Trash"; + if (lower === "junk" || lower === "spam") return dict.mailClient.junk || "Junk"; + if (lower === "archive") return dict.mailClient.archive || "Archive"; + return name; + }; // Check connection useEffect(() => { @@ -130,7 +148,7 @@ export default function MailPage() {
{email}
@@ -148,7 +166,7 @@ export default function MailPage() {

- {folders.find((f) => f.path === activeFolder)?.name ?? activeFolder} + {getFolderLabel(activeFolder)}

@@ -174,8 +192,8 @@ export default function MailPage() {
-
Bir mail seçin
-
Okumak için soldaki listeden bir mail seçin
+
{dict.mailClient.selectMail || "Bir mail seçin"}
+
{dict.mailClient.selectMailDesc || "Okumak için soldaki listeden bir mail seçin"}
)}
diff --git a/app/[lang]/dashboard/mailboxes/page.tsx b/app/[lang]/dashboard/mailboxes/page.tsx index 4544fa9..ca98ed9 100644 --- a/app/[lang]/dashboard/mailboxes/page.tsx +++ b/app/[lang]/dashboard/mailboxes/page.tsx @@ -3,6 +3,7 @@ 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; @@ -32,6 +33,7 @@ export default function MailboxesPage() { const [createForm, setCreateForm] = useState({ local_part: "", name: "", password: "", quota: 3072 }); const [newPassword, setNewPassword] = useState(""); const [formError, setFormError] = useState(""); + const dict = useDictionary(); useEffect(() => { fetch("/api/domains") @@ -81,7 +83,7 @@ export default function MailboxesPage() { }; const handleDelete = (username: string) => { - if (!confirm(`"${username}" hesabını silmek istediğinizden emin misiniz?`)) return; + 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)); @@ -128,11 +130,11 @@ export default function MailboxesPage() { <>
-

Mail Hesapları

+

{dict.mailboxes.title || "Mail Hesapları"}

{selectedDomain - ? `${selectedDomain} — ${mailboxes.length} hesap` - : "Domain seçin"} + ? `${selectedDomain} — ${mailboxes.length} ${dict.mailboxes.accounts || "hesap"}` + : (dict.mailboxes.selectDomain || "Domain seçin")}

@@ -160,7 +162,7 @@ export default function MailboxesPage() { onClick={() => setShowCreateModal(true)} disabled={!selectedDomain} > - Hesap Ekle + {dict.mailboxes.addAccount || "Hesap Ekle"}
@@ -172,7 +174,7 @@ export default function MailboxesPage() { setSearch(e.target.value)} /> @@ -188,21 +190,21 @@ export default function MailboxesPage() {
- {selectedDomain ? "Bu domainde mail hesabı yok" : "Domain seçin"} + {selectedDomain ? (dict.mailboxes.noMailboxes || "Bu domainde mail hesabı yok") : (dict.mailboxes.selectDomain || "Domain seçin")}
- {selectedDomain ? '"Hesap Ekle" butonuna tıklayın' : "Sol üstteki listeden domain seçin"} + {selectedDomain ? (dict.mailboxes.noMailboxesDesc || "\"Hesap Ekle\" butonuna tıklayın") : (dict.mailboxes.selectDomainDesc || "Sol üstteki listeden domain seçin")}
) : ( - - - - - + + + + + @@ -237,7 +239,7 @@ export default function MailboxesPage() {
E-postaAd SoyadKotaDurumİşlemler{dict.mailboxes.email || "E-posta"}{dict.mailboxes.name || "Ad Soyad"}{dict.mailboxes.quota || "Kota"}{dict.mailboxes.status || "Durum"}{dict.mailboxes.actions || "İşlemler"}
- {String(m.active) === "1" ? "● Aktif" : "● Pasif"} + {String(m.active) === "1" ? `● ${dict.mailboxes.active || "Aktif"}` : `● ${dict.mailboxes.inactive || "Pasif"}`} @@ -245,28 +247,28 @@ export default function MailboxesPage() { @@ -286,14 +288,14 @@ export default function MailboxesPage() {
e.target === e.currentTarget && setShowCreateModal(false)}>
-

Yeni Mail Hesabı

+

{dict.mailboxes.newAccount || "Yeni Mail Hesabı"}

{formError &&
{formError}
}
- +
- + setCreateForm({ ...createForm, name: e.target.value })} required />
- - {dict.mailboxes.password || "Şifre"} + setCreateForm({ ...createForm, password: e.target.value })} required />
- + setCreateForm({ ...createForm, quota: parseInt(e.target.value) || 3072 })} />
- +
@@ -348,26 +350,26 @@ export default function MailboxesPage() {
e.target === e.currentTarget && setShowPasswordModal(null)}>
-

Şifre Değiştir

+

{dict.mailboxes.changePassword || "Şifre Değiştir"}

- {showPasswordModal} için yeni şifre + {showPasswordModal} {dict.mailboxes.newPasswordFor || "için yeni şifre"}
- - {dict.mailboxes.password || "Yeni Şifre"} + setNewPassword(e.target.value)} required autoFocus />
- +
@@ -379,49 +381,49 @@ export default function MailboxesPage() {
e.target === e.currentTarget && setShowInfoModal(null)}>
-

İstemci Bağlantı Bilgileri

+

{dict.mailboxes.connectionInfo || "İstemci Bağlantı Bilgileri"}

- {showInfoModal} hesabını Apple Mail, Outlook veya telefonunuza kurmak için aşağıdaki bilgileri kullanın: + {showInfoModal} {dict.mailboxes.connectionInfoDesc || "hesabını kurmak için aşağıdaki bilgileri kullanın:"}
-
IMAP (Gelen Sunucu)
+
{dict.mailboxes.imap || "IMAP (Gelen Sunucu)"}
- Sunucu: mail.ayris.tech + {dict.mailboxes.server || "Sunucu"}: mail.ayris.tech
- +
-
Port: 993 (SSL/TLS)
+
{dict.mailboxes.port || "Port"}: 993 (SSL/TLS)
-
SMTP (Giden Sunucu)
+
{dict.mailboxes.smtp || "SMTP (Giden Sunucu)"}
- Sunucu: mail.ayris.tech + {dict.mailboxes.server || "Sunucu"}: mail.ayris.tech
- +
-
Port: 587 (STARTTLS) veya 465 (SSL)
+
{dict.mailboxes.port || "Port"}: 587 (STARTTLS) veya 465 (SSL)
-
Kimlik Doğrulama
+
{dict.mailboxes.auth || "Kimlik Doğrulama"}
- Kullanıcı Adı: {showInfoModal} - + {dict.mailboxes.username || "Kullanıcı Adı"}: {showInfoModal} +
-
Şifre: Hesap oluştururken belirlediğiniz şifre
+
{dict.mailboxes.password || "Şifre"}: {dict.mailboxes.authPassword || "Hesap oluştururken belirlediğiniz şifre"}
- +
diff --git a/app/[lang]/dashboard/page.tsx b/app/[lang]/dashboard/page.tsx index 1357455..5b3f2c2 100644 --- a/app/[lang]/dashboard/page.tsx +++ b/app/[lang]/dashboard/page.tsx @@ -2,11 +2,20 @@ import { auth } from "@/auth"; import { getDomains } from "@/lib/mailcow"; import { canAccessDomain } from "@/lib/users"; import { formatBytes } from "@/lib/format"; +import { getDictionary, Locale } from "@/app/dictionaries"; + +export default async function DashboardPage( + props: { + params: Promise<{ lang: string }>; + } +) { + const params = await props.params; -export default async function DashboardPage() { const session = await auth(); const role = session?.user?.role; const userDomains = session?.user?.domains ?? []; + const lang = params.lang as Locale; + const dict = await getDictionary(lang); const allDomains = await getDomains(); const visibleDomains = allDomains.filter((d) => canAccessDomain(userDomains, d.domain_name)); @@ -18,36 +27,36 @@ export default async function DashboardPage() { <>
-

Dashboard

-

Hoş geldiniz, {session?.user?.name} 👋

+

{dict.dashboard.title || "Dashboard"}

+

{dict.dashboard.welcome || "Hoş geldiniz"}, {session?.user?.name} 👋

} /> } /> } /> {role === "SUPER_ADMIN" && ( } /> @@ -59,17 +68,17 @@ export default async function DashboardPage() {

- Domain Durumu + {dict.dashboard.domainStatus || "Domain Durumu"}

- - - - + + + + @@ -104,7 +113,7 @@ export default async function DashboardPage() { @@ -119,31 +128,31 @@ export default async function DashboardPage() { {/* Quick actions */}

- Hızlı İşlemler + {dict.dashboard.quickActions || "Hızlı İşlemler"}

{role === "SUPER_ADMIN" && ( } - title="Domain Yönetimi" - desc="Domain ekle, sil, yönet" + title={dict.dashboard.manageDomains || "Domain Yönetimi"} + desc={dict.dashboard.manageDomainsDesc || "Domain ekle, sil, yönet"} color="var(--accent)" /> )} } - title="Mail Hesapları" - desc="Yeni hesap oluştur, şifre değiştir, sil" + title={dict.dashboard.manageMailboxes || "Mail Hesapları"} + desc={dict.dashboard.manageMailboxesDesc || "Yeni hesap oluştur, şifre değiştir, sil"} color="var(--success)" /> {role === "SUPER_ADMIN" && ( } - title="Kullanıcılar" - desc=".env'den tanımlı panel kullanıcılarını görüntüle" + title={dict.dashboard.manageUsers || "Kullanıcılar"} + desc={dict.dashboard.manageUsersDesc || ".env'den tanımlı panel kullanıcılarını görüntüle"} color="var(--warning)" /> )} diff --git a/app/[lang]/dashboard/users/page.tsx b/app/[lang]/dashboard/users/page.tsx index de6dda2..eeda6fa 100644 --- a/app/[lang]/dashboard/users/page.tsx +++ b/app/[lang]/dashboard/users/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useDictionary } from "@/components/DictionaryContext"; interface User { id: string; @@ -14,6 +15,7 @@ export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); + const dict = useDictionary(); useEffect(() => { fetch("/api/users") @@ -35,8 +37,8 @@ export default function UsersPage() { <>
-

Kullanıcılar

-

Panel kullanıcıları .env dosyasından yönetilir

+

{dict.users.title || "Kullanıcılar"}

+

{dict.users.subtitle || "Panel kullanıcıları .env dosyasından yönetilir"}

@@ -47,12 +49,16 @@ export default function UsersPage() {
-
Kullanıcı yönetimi hakkında
+
{dict.users.info ? "Info" : "Kullanıcı yönetimi hakkında"}
- Kullanıcılar .env dosyasındaki{" "} - USER_0_*,{" "} - USER_1_*… değişkenleriyle tanımlanır. - Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın. + {dict.users.info || ( + <> + Kullanıcılar .env dosyasındaki{" "} + USER_0_*,{" "} + USER_1_*… değişkenleriyle tanımlanır. + Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın. + + )}
USER_2_NAME="Ahmet Yılmaz"
@@ -70,7 +76,7 @@ export default function UsersPage() { setSearch(e.target.value)} /> @@ -85,15 +91,15 @@ export default function UsersPage() { ) : filtered.length === 0 ? (
-
Kullanıcı bulunamadı
+
{dict.users.noUsers || "Kullanıcı bulunamadı"}
) : (
DomainMail KutularıKota KullanımıDurum{dict.dashboard.domain || "Domain"}{dict.dashboard.mailboxes || "Mail Kutuları"}{dict.dashboard.quotaUsage || "Kota Kullanımı"}{dict.dashboard.status || "Durum"}
- {String(d.active) === "1" ? "● Aktif" : "● Pasif"} + {String(d.active) === "1" ? `● ${dict.dashboard.active || "Aktif"}` : `● ${dict.dashboard.inactive || "Pasif"}`}
- - - + + + @@ -112,12 +118,12 @@ export default function UsersPage() {
KullanıcıRolİzin Verilen Domainler{dict.users.username || "Kullanıcı"}{dict.users.role || "Rol"}{dict.users.domains || "İzin Verilen Domainler"}
- {u.role === "SUPER_ADMIN" ? "★ Süper Admin" : "Domain Admin"} + {u.role === "SUPER_ADMIN" ? `★ ${dict.users.superAdmin || "Süper Admin"}` : (dict.users.domainAdmin || "Domain Admin")} {u.domains.includes("*") ? ( - Tüm domainler + {dict.users.allDomains || "Tüm domainler"} ) : (
{u.domains.map((d) => ( diff --git a/app/[lang]/login/LoginForm.tsx b/app/[lang]/login/LoginForm.tsx index a2ad9f6..ca5bf06 100644 --- a/app/[lang]/login/LoginForm.tsx +++ b/app/[lang]/login/LoginForm.tsx @@ -23,7 +23,7 @@ export default function LoginForm({ dict, lang }: { dict: any; lang: string }) { }); if (res?.error) { - setError("E-posta veya şifre hatalı."); // We can translate this later + setError(dict.errorInvalid || "E-posta veya şifre hatalı."); } else { router.push(`/${lang}/dashboard`); router.refresh(); diff --git a/app/api/domains/route.ts b/app/api/domains/route.ts index 557fed6..c15a311 100644 --- a/app/api/domains/route.ts +++ b/app/api/domains/route.ts @@ -30,5 +30,13 @@ export async function POST(req: NextRequest) { if (!domain) return NextResponse.json({ error: "domain gerekli" }, { status: 400 }); const result = await createDomain({ domain, description, mailboxes, quota, maxquota }); + + if (result.ok && Array.isArray(result.data)) { + const hasError = result.data.some((item: any) => item.type === "error"); + if (hasError) { + return NextResponse.json(result.data, { status: 400 }); + } + } + return NextResponse.json(result.data, { status: result.ok ? 200 : 502 }); } diff --git a/app/api/webhooks/mail/route.ts b/app/api/webhooks/mail/route.ts new file mode 100644 index 0000000..6f7a3a5 --- /dev/null +++ b/app/api/webhooks/mail/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUsers } from "@/lib/users"; + +/** + * app/api/webhooks/mail/route.ts + * + * Webhook endpoint for incoming mail notifications (e.g. from Rspamd or Mailcow). + * Sends notifications to Telegram based on the recipient email. + */ + +export async function POST(req: NextRequest) { + try { + const data = await req.json(); + + // Extract basic info from the incoming payload + const recipient = (data.to || data.rcpt || "").toLowerCase(); + const sender = data.from || "Bilinmiyor"; + const subject = data.subject || "(Konu Yok)"; + + console.log(`[Mail Webhook] Yeni mail geldi: ${sender} -> ${recipient}`); + + const users = getUsers(); + const user = users.find(u => u.email.toLowerCase() === recipient); + const targetChatId = user?.telegramId; + + if (targetChatId && process.env.TELEGRAM_BOT_TOKEN) { + const message = `🔔 *Yeni Mail!*\n\n*Kimden:* ${sender}\n*Konu:* ${subject}`; + + const telegramUrl = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`; + + const res = await fetch(telegramUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: targetChatId, + text: message, + parse_mode: "Markdown", + }), + }); + + if (!res.ok) { + const errorText = await res.text(); + console.error(`[Mail Webhook] Telegram API hatası: ${res.status} ${errorText}`); + } else { + console.log(`[Mail Webhook] Telegram bildirimi gönderildi: ${recipient}`); + } + } else { + if (!targetChatId) { + console.log(`[Mail Webhook] Alıcı için eşleşen Telegram ID bulunamadı: ${recipient}`); + } + if (!process.env.TELEGRAM_BOT_TOKEN) { + console.error("[Mail Webhook] TELEGRAM_BOT_TOKEN ayarlı değil!"); + } + } + + return NextResponse.json({ status: "ok" }); + } catch (error: any) { + console.error(`[Mail Webhook] Hata: ${error.message}`); + return NextResponse.json({ error: "İşlem başarısız" }, { status: 500 }); + } +} diff --git a/app/dictionaries/en.json b/app/dictionaries/en.json index fd1d254..e0deba9 100644 --- a/app/dictionaries/en.json +++ b/app/dictionaries/en.json @@ -7,12 +7,148 @@ "passwordLabel": "Password", "passwordPlaceholder": "Enter your password", "signInButton": "Sign In", - "signingIn": "Signing In..." + "signingIn": "Signing In...", + "errorInvalid": "Invalid email or password." }, "sidebar": { "mailboxes": "Mailboxes", "mailClient": "Mail Client", "users": "User Management", - "logout": "Log Out" + "logout": "Log Out", + "general": "GENERAL", + "management": "MANAGEMENT", + "superAdmin": "Super Admin", + "domainAdmin": "Domain Admin" + }, + "dashboard": { + "title": "Dashboard", + "welcome": "Welcome", + "totalDomains": "Total Domains", + "mailboxes": "Mailboxes", + "aliases": "Aliases", + "users": "Defined Users", + "usersSub": "Users are managed from .env", + "domainStatus": "Domain Status", + "domain": "Domain", + "quotaUsage": "Quota Usage", + "status": "Status", + "active": "Active", + "inactive": "Inactive", + "quickActions": "Quick Actions", + "manageDomains": "Domain Management", + "manageDomainsDesc": "Add, delete, manage domains", + "manageMailboxes": "Mailboxes", + "manageMailboxesDesc": "Create new account, change password, delete", + "manageUsers": "Users", + "manageUsersDesc": "View defined panel users from .env" + }, + "domains": { + "title": "Domains", + "subtitle": "domain listed", + "searchPlaceholder": "Search domain...", + "domain": "Domain", + "description": "Description", + "mailboxes": "Mailboxes", + "aliases": "Aliases", + "quota": "Quota", + "status": "Status", + "actions": "Actions", + "active": "Active", + "inactive": "Inactive", + "addDomain": "Add Domain", + "refresh": "Refresh", + "noDomains": "No domains found", + "tryDiffSearch": "Try a different search term" + }, + "users": { + "title": "System Users", + "subtitle": "Admin users defined via .env file", + "info": "User management is done securely via the environment variables (.env). This panel is read-only.", + "username": "Username", + "name": "Name", + "role": "Role", + "domains": "Authorized Domains", + "superAdmin": "Super Admin", + "domainAdmin": "Domain Admin", + "allDomains": "All Domains" + }, + "mailboxes": { + "title": "Mailboxes", + "selectDomain": "Select domain", + "accounts": "accounts", + "addAccount": "Add Account", + "searchPlaceholder": "Search email or name...", + "email": "Email", + "name": "Name", + "quota": "Quota", + "status": "Status", + "actions": "Actions", + "active": "Active", + "inactive": "Inactive", + "noMailboxes": "No mailboxes in this domain", + "noMailboxesDesc": "Click 'Add Account'", + "selectDomainDesc": "Select a domain from the top left", + "deleteConfirm": "Are you sure you want to delete this account?", + "info": "Connection Info", + "changePassword": "Change Password", + "deactivate": "Deactivate", + "activate": "Activate", + "delete": "Delete", + "newAccount": "New Mailbox", + "username": "Username", + "password": "Password", + "quotaMb": "Quota (MB)", + "cancel": "Cancel", + "create": "Create", + "newPasswordFor": "New password for", + "update": "Update", + "connectionInfo": "Client Connection Info", + "connectionInfoDesc": "Use the following details to set up the account on Apple Mail, Outlook or your phone:", + "imap": "IMAP (Incoming Server)", + "server": "Server", + "port": "Port", + "copy": "Copy", + "smtp": "SMTP (Outgoing Server)", + "auth": "Authentication", + "authPassword": "The password you set when creating the account", + "ok": "OK" + }, + "mailClient": { + "loginTitle": "IMAP Login", + "loginSubtitle": "Connect to your mailbox", + "emailLabel": "Email", + "emailPlaceholder": "name@domain.com", + "passwordLabel": "Password", + "passwordPlaceholder": "********", + "connect": "Connect", + "connecting": "Connecting...", + "newMail": "New Mail", + "logout": "Logout", + "selectMail": "Select a mail", + "selectMailDesc": "Select a mail from the left list to read", + "inbox": "Inbox", + "sent": "Sent", + "drafts": "Drafts", + "trash": "Trash", + "junk": "Junk", + "archive": "Archive", + "searchMail": "Search mail...", + "emptyFolder": "This folder is empty", + "to": "To", + "cc": "Cc", + "subject": "Subject", + "date": "Date", + "attachments": "Attachments", + "reply": "Reply", + "delete": "Delete", + "download": "Download", + "composeTitle": "New Message", + "send": "Send", + "sending": "Sending...", + "cancel": "Cancel", + "dropFiles": "Drop files here or click to attach", + "sendError": "Failed to send message", + "noSubject": "(No Subject)", + "me": "Me" } } diff --git a/app/dictionaries/tr.json b/app/dictionaries/tr.json index 0c1696b..f0a23b3 100644 --- a/app/dictionaries/tr.json +++ b/app/dictionaries/tr.json @@ -7,12 +7,148 @@ "passwordLabel": "Şifre", "passwordPlaceholder": "Şifrenizi girin", "signInButton": "Giriş Yap", - "signingIn": "Giriş yapılıyor..." + "signingIn": "Giriş yapılıyor...", + "errorInvalid": "E-posta veya şifre hatalı." }, "sidebar": { "mailboxes": "Mail Hesapları", - "mailClient": "Web Mail Client", + "mailClient": "Mail İstemcisi", "users": "Kullanıcı Yönetimi", - "logout": "Çıkış Yap" + "logout": "Çıkış Yap", + "general": "GENEL", + "management": "YÖNETİM", + "superAdmin": "Süper Admin", + "domainAdmin": "Domain Admin" + }, + "dashboard": { + "title": "Dashboard", + "welcome": "Hoş geldiniz", + "totalDomains": "Toplam Domain", + "mailboxes": "Mail Kutuları", + "aliases": "Alias", + "users": "Tanımlı Kullanıcı", + "usersSub": "Kullanıcılar .env'den yönetilir", + "domainStatus": "Domain Durumu", + "domain": "Domain", + "quotaUsage": "Kota Kullanımı", + "status": "Durum", + "active": "Aktif", + "inactive": "Pasif", + "quickActions": "Hızlı İşlemler", + "manageDomains": "Domain Yönetimi", + "manageDomainsDesc": "Domain ekle, sil, yönet", + "manageMailboxes": "Mail Hesapları", + "manageMailboxesDesc": "Yeni hesap oluştur, şifre değiştir, sil", + "manageUsers": "Kullanıcılar", + "manageUsersDesc": ".env'den tanımlı panel kullanıcılarını görüntüle" + }, + "domains": { + "title": "Domainler", + "subtitle": "domain listeleniyor", + "searchPlaceholder": "Domain ara...", + "domain": "Domain", + "description": "Açıklama", + "mailboxes": "Mail Kutuları", + "aliases": "Alias", + "quota": "Kota", + "status": "Durum", + "actions": "İşlemler", + "active": "Aktif", + "inactive": "Pasif", + "addDomain": "Domain Ekle", + "refresh": "Yenile", + "noDomains": "Domain bulunamadı", + "tryDiffSearch": "Farklı bir arama yapın" + }, + "users": { + "title": "Sistem Kullanıcıları", + "subtitle": ".env dosyası üzerinden tanımlanmış yetkili kullanıcılar", + "info": "Kullanıcı yönetimi güvenlik nedeniyle sadece çevresel değişkenler (.env) üzerinden yapılmaktadır. Bu ekran salt okunurdur.", + "username": "Kullanıcı Adı", + "name": "Ad Soyad", + "role": "Rol", + "domains": "Yetkili Domainler", + "superAdmin": "Süper Admin", + "domainAdmin": "Domain Admin", + "allDomains": "Tüm Domainler" + }, + "mailboxes": { + "title": "Mail Hesapları", + "selectDomain": "Domain seçin", + "accounts": "hesap", + "addAccount": "Hesap Ekle", + "searchPlaceholder": "E-posta veya isim ara...", + "email": "E-posta", + "name": "Ad Soyad", + "quota": "Kota", + "status": "Durum", + "actions": "İşlemler", + "active": "Aktif", + "inactive": "Pasif", + "noMailboxes": "Bu domainde mail hesabı yok", + "noMailboxesDesc": "'Hesap Ekle' butonuna tıklayın", + "selectDomainDesc": "Sol üstteki listeden domain seçin", + "deleteConfirm": "hesabını silmek istediğinizden emin misiniz?", + "info": "Bağlantı Bilgileri", + "changePassword": "Şifre Değiştir", + "deactivate": "Pasife Al", + "activate": "Aktif Et", + "delete": "Sil", + "newAccount": "Yeni Mail Hesabı", + "username": "Kullanıcı Adı", + "password": "Şifre", + "quotaMb": "Kota (MB)", + "cancel": "İptal", + "create": "Oluştur", + "newPasswordFor": "için yeni şifre", + "update": "Güncelle", + "connectionInfo": "İstemci Bağlantı Bilgileri", + "connectionInfoDesc": "hesabını Apple Mail, Outlook veya telefonunuza kurmak için aşağıdaki bilgileri kullanın:", + "imap": "IMAP (Gelen Sunucu)", + "server": "Sunucu", + "port": "Port", + "copy": "Kopyala", + "smtp": "SMTP (Giden Sunucu)", + "auth": "Kimlik Doğrulama", + "authPassword": "Hesap oluştururken belirlediğiniz şifre", + "ok": "Tamam" + }, + "mailClient": { + "loginTitle": "IMAP Girişi", + "loginSubtitle": "Mail kutunuza bağlanın", + "emailLabel": "E-posta", + "emailPlaceholder": "isim@domain.com", + "passwordLabel": "Şifre", + "passwordPlaceholder": "********", + "connect": "Bağlan", + "connecting": "Bağlanıyor...", + "newMail": "Yeni Mail", + "logout": "Çıkış", + "selectMail": "Bir mail seçin", + "selectMailDesc": "Okumak için soldaki listeden bir mail seçin", + "inbox": "Gelen Kutusu", + "sent": "Gönderilmiş", + "drafts": "Taslaklar", + "trash": "Çöp Kutusu", + "junk": "Gereksiz", + "archive": "Arşiv", + "searchMail": "Mail ara...", + "emptyFolder": "Bu klasör boş", + "to": "Kime", + "cc": "Bilgi", + "subject": "Konu", + "date": "Tarih", + "attachments": "Ekler", + "reply": "Yanıtla", + "delete": "Sil", + "download": "İndir", + "composeTitle": "Yeni Mesaj", + "send": "Gönder", + "sending": "Gönderiliyor...", + "cancel": "İptal", + "dropFiles": "Dosyaları sürükleyin veya dosya eklemek için tıklayın", + "sendError": "Mesaj gönderilemedi", + "noSubject": "(Konu Yok)", + "me": "Ben" } } diff --git a/app/globals.css b/app/globals.css index 2a7d952..a94a998 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1105,3 +1105,18 @@ tr:hover td { .page-header { padding: 16px; } .stats-grid { grid-template-columns: 1fr 1fr; } } +/* ── Language Switcher ── */ +.lang-switcher .btn-ghost { + border-radius: var(--radius) !important; + font-weight: 400 !important; +} + +.lang-option:hover { + background: var(--bg-hover) !important; + color: var(--text-primary) !important; +} + +.lang-option.active { + color: var(--accent-hover) !important; + font-weight: 600; +} diff --git a/components/DictionaryContext.tsx b/components/DictionaryContext.tsx new file mode 100644 index 0000000..bfa1f75 --- /dev/null +++ b/components/DictionaryContext.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React, { createContext, useContext } from "react"; + +const DictionaryContext = createContext(null); + +export function DictionaryProvider({ + dictionary, + children, +}: { + dictionary: any; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useDictionary() { + const context = useContext(DictionaryContext); + if (!context) { + throw new Error("useDictionary must be used within a DictionaryProvider"); + } + return context; +} diff --git a/components/LanguageSwitcher.tsx b/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..aa6dc77 --- /dev/null +++ b/components/LanguageSwitcher.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import { useState, useRef, useEffect } from "react"; + +export default function LanguageSwitcher({ currentLang }: { currentLang: string }) { + const pathname = usePathname(); + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const languages = [ + { code: "tr", label: "Türkçe", flag: "🇹🇷" }, + { code: "en", label: "English", flag: "🇺🇸" }, + ]; + + const handleLangChange = (newLang: string) => { + if (newLang === currentLang) { + setIsOpen(false); + return; + } + + const segments = pathname.split("/"); + segments[1] = newLang; + const newPath = segments.join("/"); + + router.push(newPath); + setIsOpen(false); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const current = languages.find((l) => l.code === currentLang) || languages[0]; + + return ( +
+ + + {isOpen && ( +
+ {languages.map((lang) => ( + + ))} +
+ )} +
+ ); +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index aacbcb1..ff81582 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -3,6 +3,7 @@ import { useSession, signOut } from "next-auth/react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import LanguageSwitcher from "./LanguageSwitcher"; export default function Sidebar({ dict, lang }: { dict: any; lang: string }) { const { data: session } = useSession(); @@ -13,18 +14,18 @@ export default function Sidebar({ dict, lang }: { dict: any; lang: string }) { const navItems = [ { - section: "GENEL", + section: dict.sidebar?.general || "GENEL", items: [ - { href: `/${lang}/dashboard`, label: "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, - { href: `/${lang}/dashboard/mail`, label: dict.mailClient || "Mail", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, + { href: `/${lang}/dashboard`, label: dict.dashboard?.title || "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, + { href: `/${lang}/dashboard/mail`, label: dict.sidebar?.mailClient || "Mail Client", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, ], }, { - section: "YÖNETİM", + section: dict.sidebar?.management || "YÖNETİM", items: [ - { href: `/${lang}/dashboard/domains`, label: "Domainler", icon: GlobeIcon, roles: ["SUPER_ADMIN"] }, - { href: `/${lang}/dashboard/users`, label: dict.users || "Kullanıcılar", icon: UsersIcon, roles: ["SUPER_ADMIN"] }, - { href: `/${lang}/dashboard/mailboxes`, label: dict.mailboxes || "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, + { href: `/${lang}/dashboard/domains`, label: dict.domains?.title || "Domainler", icon: GlobeIcon, roles: ["SUPER_ADMIN"] }, + { href: `/${lang}/dashboard/users`, label: dict.sidebar?.users || "Kullanıcılar", icon: UsersIcon, roles: ["SUPER_ADMIN"] }, + { href: `/${lang}/dashboard/mailboxes`, label: dict.sidebar?.mailboxes || "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, ], }, ]; @@ -68,23 +69,26 @@ export default function Sidebar({ dict, lang }: { dict: any; lang: string }) {
+ +
{name[0]?.toUpperCase()}
{name}
-
{role === "SUPER_ADMIN" ? "Süper Admin" : "Domain Admin"}
+
{role === "SUPER_ADMIN" ? (dict.superAdmin || "Süper Admin") : (dict.domainAdmin || "Domain Admin")}
diff --git a/components/mail/ComposeModal.tsx b/components/mail/ComposeModal.tsx index 8657487..3e6beed 100644 --- a/components/mail/ComposeModal.tsx +++ b/components/mail/ComposeModal.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useCallback } from "react"; import type { MailMessage } from "@/app/[lang]/dashboard/mail/page"; import { formatBytes } from "@/lib/format"; +import { useDictionary } from "@/components/DictionaryContext"; interface AttachmentFile { file: File; @@ -14,6 +15,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: { onClose: () => void; onSent: () => void; }) { + const dict = useDictionary(); const [to, setTo] = useState(replyTo ? replyTo.from[0]?.address ?? "" : ""); const [cc, setCc] = useState(""); const [subject, setSubject] = useState(replyTo ? `Re: ${replyTo.subject.replace(/^Re:\s*/i, "")}` : ""); @@ -60,7 +62,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: { const res = await fetch("/api/mail/send", { method: "POST", body: formData }); const data = await res.json(); - if (!res.ok) throw new Error(data.error || "Gönderilemedi"); + if (!res.ok) throw new Error(data.error || dict.mailClient.sendError || "Gönderilemedi"); } else { // JSON for simple messages const res = await fetch("/api/mail/send", { @@ -72,7 +74,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: { }), }); const data = await res.json(); - if (!res.ok) throw new Error(data.error || "Gönderilemedi"); + if (!res.ok) throw new Error(data.error || dict.mailClient.sendError || "Gönderilemedi"); } onSent(); } catch (err: any) { @@ -87,24 +89,24 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
e.target === e.currentTarget && onClose()}>
-

{replyTo ? "Yanıtla" : "Yeni Mail"}

+

{replyTo ? (dict.mailClient.reply || "Yanıtla") : (dict.mailClient.composeTitle || "Yeni Mail")}

{error &&
{error}
}
- + setTo(e.target.value)} required autoFocus />
- + setCc(e.target.value)} />
- + setSubject(e.target.value)} required />
@@ -131,7 +133,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: { onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }} /> 📎 - Dosya sürükleyin veya tıklayın + {dict.mailClient.dropFiles || "Dosya sürükleyin veya tıklayın"}
{/* Attachment list */} @@ -145,15 +147,15 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
))}
- Toplam: {formatBytes(totalSize)} — {attachments.length} dosya + {formatBytes(totalSize)} — {attachments.length} {dict.mailClient.attachments || "dosya"}
)}
- +
diff --git a/components/mail/FolderList.tsx b/components/mail/FolderList.tsx index 8e707ad..5095191 100644 --- a/components/mail/FolderList.tsx +++ b/components/mail/FolderList.tsx @@ -1,5 +1,6 @@ "use client"; import type { MailFolder } from "@/app/[lang]/dashboard/mail/page"; +import { useDictionary } from "@/components/DictionaryContext"; const FOLDER_ICONS: Record = { "\\Inbox": "📥", @@ -10,15 +11,6 @@ const FOLDER_ICONS: Record = { "\\Archive": "📦", }; -const FOLDER_LABELS: Record = { - INBOX: "Gelen Kutusu", - Sent: "Gönderilenler", - Drafts: "Taslaklar", - Trash: "Çöp Kutusu", - Junk: "Spam", - Archive: "Arşiv", -}; - function getFolderIcon(folder: MailFolder): string { if (folder.specialUse && FOLDER_ICONS[folder.specialUse]) return FOLDER_ICONS[folder.specialUse]; const lower = folder.path.toLowerCase(); @@ -31,15 +23,25 @@ function getFolderIcon(folder: MailFolder): string { return "📁"; } -function getFolderLabel(folder: MailFolder): string { - return FOLDER_LABELS[folder.name] ?? FOLDER_LABELS[folder.path] ?? folder.name; -} - export default function FolderList({ folders, active, onSelect }: { folders: MailFolder[]; active: string; onSelect: (path: string) => void; }) { + const dict = useDictionary(); + + const getFolderLabel = (folder: MailFolder): string => { + const name = folder.name || folder.path; + const lower = name.toLowerCase(); + if (lower === "inbox") return dict.mailClient.inbox || "Inbox"; + if (lower === "sent") return dict.mailClient.sent || "Sent"; + if (lower === "drafts") return dict.mailClient.drafts || "Drafts"; + if (lower === "trash") return dict.mailClient.trash || "Trash"; + if (lower === "junk" || lower === "spam") return dict.mailClient.junk || "Junk"; + if (lower === "archive") return dict.mailClient.archive || "Archive"; + return name; + }; + const sorted = [...folders].sort((a, b) => { if (a.path === "INBOX") return -1; if (b.path === "INBOX") return 1; diff --git a/components/mail/MailLogin.tsx b/components/mail/MailLogin.tsx index 5660538..cfb7aec 100644 --- a/components/mail/MailLogin.tsx +++ b/components/mail/MailLogin.tsx @@ -1,11 +1,13 @@ "use client"; import { useState } from "react"; +import { useDictionary } from "@/components/DictionaryContext"; export default function MailLogin({ onSuccess }: { onSuccess: (email: string) => void }) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const dict = useDictionary(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -30,29 +32,26 @@ export default function MailLogin({ onSuccess }: { onSuccess: (email: string) =>
📧
-

Mail Hesabına Bağlan

+

{dict.mailClient.loginTitle || "IMAP Girişi"}

- Mailcow mail hesabınızın bilgilerini girin + {dict.mailClient.loginSubtitle || "Mail kutunuza bağlanın"}

{error &&
{error}
}
- - {dict.mailClient.emailLabel || "E-posta"} + setEmail(e.target.value)} required autoFocus />
- - {dict.mailClient.passwordLabel || "Şifre"} + setPassword(e.target.value)} required />
-

- Şifreniz sunucuda saklanmaz, sadece oturum süresince kullanılır. -

diff --git a/components/mail/MessageList.tsx b/components/mail/MessageList.tsx index 5337c53..7da04f6 100644 --- a/components/mail/MessageList.tsx +++ b/components/mail/MessageList.tsx @@ -1,23 +1,25 @@ "use client"; import type { MailEnvelope } from "@/app/[lang]/dashboard/mail/page"; +import { useDictionary } from "@/components/DictionaryContext"; +import { useParams } from "next/navigation"; -function timeAgo(dateStr: string): string { +function timeAgo(dateStr: string, lang: string): string { const now = new Date(); const d = new Date(dateStr); const diff = now.getTime() - d.getTime(); const mins = Math.floor(diff / 60000); - if (mins < 1) return "şimdi"; - if (mins < 60) return `${mins}dk`; + if (mins < 1) return lang === "tr" ? "şimdi" : "now"; + if (mins < 60) return `${mins}${lang === "tr" ? "dk" : "m"}`; const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}sa`; + if (hrs < 24) return `${hrs}${lang === "tr" ? "sa" : "h"}`; const days = Math.floor(hrs / 24); - if (days < 7) return `${days}g`; - return d.toLocaleDateString("tr-TR", { day: "numeric", month: "short" }); + if (days < 7) return `${days}${lang === "tr" ? "g" : "d"}`; + return d.toLocaleDateString(lang === "tr" ? "tr-TR" : "en-US", { day: "numeric", month: "short" }); } function senderName(msg: MailEnvelope): string { const f = msg.from[0]; - return f?.name || f?.address || "Bilinmeyen"; + return f?.name || f?.address || "Unknown"; } export default function MessageList({ messages, loading, selectedUid, onSelect, onDelete }: { @@ -27,6 +29,10 @@ export default function MessageList({ messages, loading, selectedUid, onSelect, onSelect: (uid: number) => void; onDelete: (uid: number) => void; }) { + const dict = useDictionary(); + const params = useParams(); + const lang = (params.lang as string) || "en"; + if (loading) { return
; } @@ -34,7 +40,7 @@ export default function MessageList({ messages, loading, selectedUid, onSelect, if (messages.length === 0) { return (
-
Bu klasörde mail yok
+
{dict.mailClient.emptyFolder || "Bu klasör boş"}
); } @@ -53,11 +59,11 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
{senderName(m)} - {timeAgo(m.date)} + {timeAgo(m.date, lang)}
-
{m.subject}
+
{m.subject || dict.mailClient.noSubject || "(Konu Yok)"}
- {m.hasAttachments && 📎} + {m.hasAttachments && 📎} ))} diff --git a/components/mail/MessageView.tsx b/components/mail/MessageView.tsx index ebeb141..203dd96 100644 --- a/components/mail/MessageView.tsx +++ b/components/mail/MessageView.tsx @@ -1,6 +1,8 @@ "use client"; import type { MailMessage } from "@/app/[lang]/dashboard/mail/page"; import { formatBytes } from "@/lib/format"; +import { useDictionary } from "@/components/DictionaryContext"; +import { useParams } from "next/navigation"; function getFileIcon(contentType: string, filename: string): string { if (contentType.startsWith("image/")) return "🖼️"; @@ -24,8 +26,11 @@ export default function MessageView({ message, onReply, onDelete, folder }: { onDelete: () => void; folder: string; }) { + const dict = useDictionary(); + const params = useParams(); + const lang = (params.lang as string) || "en"; const from = message.from[0]; - const date = new Date(message.date).toLocaleString("tr-TR", { + const date = new Date(message.date).toLocaleString(lang === "tr" ? "tr-TR" : "en-US", { day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }); @@ -48,13 +53,13 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
{/* Header */}
-

{message.subject}

+

{message.subject || dict.mailClient.noSubject || "(Konu Yok)"}

- -
@@ -76,7 +81,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: { {message.attachments.length > 0 && (
- 📎 {message.attachments.length} ek + 📎 {message.attachments.length} {dict.mailClient.attachments || "ek"}
{message.attachments.map((att, i) => (
handleAttachment(att, false)}> @@ -89,7 +94,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: { diff --git a/lib/mail-session.ts b/lib/mail-session.ts index 1773363..d61d4a5 100644 --- a/lib/mail-session.ts +++ b/lib/mail-session.ts @@ -1,16 +1,12 @@ -/** - * lib/mail-session.ts - * Stores/retrieves mail credentials in an encrypted httpOnly cookie. - * Credentials never hit the database. - */ - import { cookies } from "next/headers"; +import { auth } from "@/auth"; const COOKIE_NAME = "ayrismail_creds"; export interface MailSessionData { email: string; password: string; + ownerId: string; // Dashboard user ID who owns this mail session } /** @@ -30,9 +26,19 @@ function decode(token: string): MailSessionData | null { } /** Save mail credentials to cookie */ -export async function setMailSession(data: MailSessionData): Promise { +export async function setMailSession(data: Omit): Promise { + const session = await auth(); + if (!session?.user?.id) { + throw new Error("Cannot set mail session without dashboard session"); + } + const cookieStore = await cookies(); - cookieStore.set(COOKIE_NAME, encode(data), { + const sessionData: MailSessionData = { + ...data, + ownerId: session.user.id, + }; + + cookieStore.set(COOKIE_NAME, encode(sessionData), { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", @@ -43,10 +49,24 @@ export async function setMailSession(data: MailSessionData): Promise { /** Get mail credentials from cookie */ export async function getMailSession(): Promise { + const session = await auth(); + if (!session?.user?.id) return null; + const cookieStore = await cookies(); const cookie = cookieStore.get(COOKIE_NAME); if (!cookie?.value) return null; - return decode(cookie.value); + + const data = decode(cookie.value); + if (!data) return null; + + // Verify that this mail session belongs to the current dashboard user + if (data.ownerId !== session.user.id) { + // Session belongs to another user, clear it for safety + await clearMailSession(); + return null; + } + + return data; } /** Clear mail credentials cookie */ diff --git a/lib/mailcow.ts b/lib/mailcow.ts index 8820303..dd55fdd 100644 --- a/lib/mailcow.ts +++ b/lib/mailcow.ts @@ -4,22 +4,38 @@ * Uses the single super-admin API key from .env. */ -const BASE = process.env.MAILCOW_API_URL?.replace(/\/$/, "") ?? ""; +let BASE = process.env.MAILCOW_API_URL?.replace(/\/$/, "") ?? ""; +if (BASE && !BASE.startsWith("http")) { + BASE = `https://${BASE}`; +} const KEY = process.env.MAILCOW_API_KEY ?? ""; async function mfetch(path: string, options: RequestInit = {}) { + if (!BASE || !KEY) { + console.error("[Mailcow API] MAILCOW_API_URL or MAILCOW_API_KEY is not set in .env"); + return new Response(JSON.stringify({ error: "Server configuration error" }), { status: 500 }); + } const url = `${BASE}/api/v1${path}`; - const res = await fetch(url, { - ...options, - headers: { - "Content-Type": "application/json", - "X-API-Key": KEY, - ...options.headers, - }, - // Don't cache — always fresh - cache: "no-store", - }); - return res; + console.log(`[Mailcow API] ${options.method || "GET"} ${url}`); + try { + const res = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + "X-API-Key": KEY, + ...options.headers, + }, + cache: "no-store", + }); + if (!res.ok) { + const text = await res.text(); + console.error(`[Mailcow API] Error ${res.status}: ${text}`); + } + return res; + } catch (err: any) { + console.error(`[Mailcow API] Fetch failed: ${err.message}`); + return new Response(JSON.stringify({ error: "Connection to Mailcow failed" }), { status: 503 }); + } } // ─── Types ───────────────────────────────────────────────── @@ -88,11 +104,14 @@ export async function createDomain(payload: { active: 1, aliases: 400, mailboxes: 10, - defquota: 3072, - maxquota: 10240, + defquota: Math.min(payload.quota || 10240, 3072), + maxquota: payload.quota || 10240, quota: 10240, ...payload, }; + // Double check constraints + if (body.maxquota > body.quota) body.maxquota = body.quota; + if (body.defquota > body.maxquota) body.defquota = body.maxquota; const res = await mfetch("/add/domain", { method: "POST", body: JSON.stringify(body) }); const data = await res.json(); return { ok: res.ok, data }; diff --git a/lib/users.ts b/lib/users.ts index 4bf8952..d86a017 100644 --- a/lib/users.ts +++ b/lib/users.ts @@ -23,6 +23,7 @@ export interface AppUser { password: string; // plain text — store hashed in prod or use secrets manager role: "SUPER_ADMIN" | "DOMAIN_ADMIN"; domains: string[]; // ["*"] for super admin, ["domain.com"] for domain admins + telegramId?: string; // Optional Telegram ID for notifications } /** Load all users defined in environment variables */ @@ -36,6 +37,7 @@ export function getUsers(): AppUser[] { const password = process.env[`USER_${i}_PASSWORD`]; const role = process.env[`USER_${i}_ROLE`] as AppUser["role"]; const domainsRaw = process.env[`USER_${i}_DOMAINS`] ?? ""; + const telegramId = process.env[`USER_${i}_TELEGRAM_ID`]; if (!name || !email || !password) break; @@ -46,6 +48,7 @@ export function getUsers(): AppUser[] { password, role: role ?? "DOMAIN_ADMIN", domains: domainsRaw === "*" ? ["*"] : domainsRaw.split(",").map((d) => d.trim()).filter(Boolean), + telegramId, }); i++;