feat: complete i18n support, telegram webhook, and security improvements
This commit is contained in:
@@ -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() {
|
||||
<>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Domainler</h1>
|
||||
<p className="page-subtitle">Mailcow üzerindeki tüm domainleri yönetin</p>
|
||||
<h1 className="page-title">{dict.domains.title || "Domainler"}</h1>
|
||||
<p className="page-subtitle">{domains.length} {dict.domains.subtitle || "domain listeleniyor"}</p>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => setShowModal(true)}>
|
||||
<PlusIcon /> Domain Ekle
|
||||
<PlusIcon /> {dict.domains.addDomain || "Domain Ekle"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -93,13 +104,13 @@ export default function DomainsPage() {
|
||||
<input
|
||||
type="text"
|
||||
className="input search-input"
|
||||
placeholder="Domain veya açıklama ara..."
|
||||
placeholder={dict.domains.searchPlaceholder || "Domain ara..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-ghost" onClick={fetchDomains}>
|
||||
<RefreshIcon /> Yenile
|
||||
<RefreshIcon /> {dict.domains.refresh || "Yenile"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -109,19 +120,19 @@ export default function DomainsPage() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon"><GlobeIcon size={24} /></div>
|
||||
<div style={{ fontWeight: 600 }}>Domain bulunamadı</div>
|
||||
<div style={{ fontSize: 12 }}>Mailcow'a domain eklemek için "Domain Ekle" butonuna tıklayın.</div>
|
||||
<div style={{ fontWeight: 600 }}>{dict.domains.noDomains || "Domain bulunamadı"}</div>
|
||||
<div style={{ fontSize: 12 }}>{dict.domains.tryDiffSearch || "Farklı bir arama yapın."}</div>
|
||||
</div>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Mail Kutuları</th>
|
||||
<th>Alias</th>
|
||||
<th>Kota</th>
|
||||
<th>Durum</th>
|
||||
<th>İşlemler</th>
|
||||
<th>{dict.domains.domain || "Domain"}</th>
|
||||
<th>{dict.domains.mailboxes || "Mail Kutuları"}</th>
|
||||
<th>{dict.domains.aliases || "Alias"}</th>
|
||||
<th>{dict.domains.quota || "Kota"}</th>
|
||||
<th>{dict.domains.status || "Durum"}</th>
|
||||
<th>{dict.domains.actions || "İşlemler"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -160,7 +171,7 @@ export default function DomainsPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
|
||||
{String(d.active) === "1" ? "● Aktif" : "● Pasif"}
|
||||
{String(d.active) === "1" ? `● ${dict.domains.active || "Aktif"}` : `● ${dict.domains.inactive || "Pasif"}`}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -181,20 +192,20 @@ export default function DomainsPage() {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowModal(false)}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">Mailcow'a Domain Ekle</h2>
|
||||
<h2 className="modal-title">{dict.domains.addDomain || "Domain Ekle"}</h2>
|
||||
<button className="modal-close" onClick={() => setShowModal(false)}><XIcon /></button>
|
||||
</div>
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="modal-body form-group">
|
||||
{formError && <div className="error-msg">{formError}</div>}
|
||||
<div>
|
||||
<label className="label">Domain Adı</label>
|
||||
<label className="label">{dict.domains.domain || "Domain Adı"}</label>
|
||||
<input type="text" className="input" placeholder="ornek.com" value={form.domain}
|
||||
onChange={(e) => setForm({ ...form, domain: e.target.value })} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Açıklama (isteğe bağlı)</label>
|
||||
<input type="text" className="input" placeholder="Bu domain hakkında..." value={form.description}
|
||||
<label className="label">{dict.domains.description || "Açıklama"}</label>
|
||||
<input type="text" className="input" placeholder="" value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
|
||||
@@ -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 (
|
||||
<Providers>
|
||||
<div className="app-layout">
|
||||
<Sidebar dict={dict.sidebar} lang={params.lang} />
|
||||
<div className="main-content">{children}</div>
|
||||
</div>
|
||||
<DictionaryProvider dictionary={dict}>
|
||||
<div className="app-layout">
|
||||
<Sidebar dict={dict} lang={params.lang} />
|
||||
<div className="main-content">{children}</div>
|
||||
</div>
|
||||
</DictionaryProvider>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<boolean | null>(null);
|
||||
const [email, setEmail] = useState("");
|
||||
const [folders, setFolders] = useState<MailFolder[]>([]);
|
||||
@@ -44,6 +48,20 @@ export default function MailPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCompose, setShowCompose] = useState(false);
|
||||
const [replyTo, setReplyTo] = useState<MailMessage | null>(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() {
|
||||
<div className="mail-layout">
|
||||
<div className="mail-sidebar">
|
||||
<button className="btn btn-primary" style={{ width: "100%" }} onClick={() => { setReplyTo(null); setShowCompose(true); }}>
|
||||
<ComposeIcon /> Yeni Mail
|
||||
<ComposeIcon /> {dict.mailClient.newMail || "Yeni Mail"}
|
||||
</button>
|
||||
<FolderList
|
||||
folders={folders}
|
||||
@@ -140,7 +158,7 @@ export default function MailPage() {
|
||||
<div className="mail-account">
|
||||
<div className="mail-account-email">{email}</div>
|
||||
<button className="btn btn-ghost btn-sm" onClick={handleDisconnect} style={{ fontSize: 11 }}>
|
||||
Çıkış
|
||||
{dict.mailClient.logout || "Çıkış"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +166,7 @@ export default function MailPage() {
|
||||
<div className="mail-list">
|
||||
<div className="mail-list-header">
|
||||
<h2>
|
||||
{folders.find((f) => f.path === activeFolder)?.name ?? activeFolder}
|
||||
{getFolderLabel(activeFolder)}
|
||||
</h2>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => loadMessages(activeFolder)}>↻</button>
|
||||
</div>
|
||||
@@ -174,8 +192,8 @@ export default function MailPage() {
|
||||
<div className="mail-empty-icon">
|
||||
<MailBigIcon />
|
||||
</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: "var(--text-secondary)" }}>Bir mail seçin</div>
|
||||
<div style={{ fontSize: 12 }}>Okumak için soldaki listeden bir mail seçin</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: "var(--text-secondary)" }}>{dict.mailClient.selectMail || "Bir mail seçin"}</div>
|
||||
<div style={{ fontSize: 12 }}>{dict.mailClient.selectMailDesc || "Okumak için soldaki listeden bir mail seçin"}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Mail Hesapları</h1>
|
||||
<h1 className="page-title">{dict.mailboxes.title || "Mail Hesapları"}</h1>
|
||||
<p className="page-subtitle">
|
||||
{selectedDomain
|
||||
? `${selectedDomain} — ${mailboxes.length} hesap`
|
||||
: "Domain seçin"}
|
||||
? `${selectedDomain} — ${mailboxes.length} ${dict.mailboxes.accounts || "hesap"}`
|
||||
: (dict.mailboxes.selectDomain || "Domain seçin")}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
|
||||
@@ -160,7 +162,7 @@ export default function MailboxesPage() {
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
disabled={!selectedDomain}
|
||||
>
|
||||
<PlusIcon /> Hesap Ekle
|
||||
<PlusIcon /> {dict.mailboxes.addAccount || "Hesap Ekle"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +174,7 @@ export default function MailboxesPage() {
|
||||
<input
|
||||
type="text"
|
||||
className="input search-input"
|
||||
placeholder="E-posta veya isim ara..."
|
||||
placeholder={dict.mailboxes.searchPlaceholder || "E-posta veya isim ara..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
@@ -188,21 +190,21 @@ export default function MailboxesPage() {
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon"><MailIcon size={24} /></div>
|
||||
<div style={{ fontWeight: 600 }}>
|
||||
{selectedDomain ? "Bu domainde mail hesabı yok" : "Domain seçin"}
|
||||
{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 ? '"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")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>E-posta</th>
|
||||
<th>Ad Soyad</th>
|
||||
<th>Kota</th>
|
||||
<th>Durum</th>
|
||||
<th>İşlemler</th>
|
||||
<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>
|
||||
@@ -237,7 +239,7 @@ export default function MailboxesPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${String(m.active) === "1" ? "badge-green" : "badge-red"}`}>
|
||||
{String(m.active) === "1" ? "● Aktif" : "● Pasif"}
|
||||
{String(m.active) === "1" ? `● ${dict.mailboxes.active || "Aktif"}` : `● ${dict.mailboxes.inactive || "Pasif"}`}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -245,28 +247,28 @@ export default function MailboxesPage() {
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setShowInfoModal(m.username)}
|
||||
title="Bağlantı Bilgileri"
|
||||
title={dict.mailboxes.info || "Bağlantı Bilgileri"}
|
||||
>
|
||||
<InfoIcon />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => setShowPasswordModal(m.username)}
|
||||
title="Şifre Değiştir"
|
||||
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" ? "Pasife Al" : "Aktif Et"}
|
||||
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="Sil"
|
||||
title={dict.mailboxes.delete || "Sil"}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
@@ -286,14 +288,14 @@ export default function MailboxesPage() {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowCreateModal(false)}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">Yeni Mail Hesabı</h2>
|
||||
<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">Kullanıcı Adı</label>
|
||||
<label className="label">{dict.mailboxes.username || "Kullanıcı Adı"}</label>
|
||||
<div style={{ display: "flex" }}>
|
||||
<input
|
||||
type="text" className="input"
|
||||
@@ -313,29 +315,29 @@ export default function MailboxesPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Ad Soyad</label>
|
||||
<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">Şifre</label>
|
||||
<input type="password" className="input" placeholder="Güçlü bir şifre"
|
||||
<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">Kota (MB)</label>
|
||||
<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>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-ghost" onClick={() => setShowCreateModal(false)}>İptal</button>
|
||||
<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 />} Oluştur
|
||||
{isPending ? <span className="spinner" /> : <PlusIcon />} {dict.mailboxes.create || "Oluştur"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -348,26 +350,26 @@ export default function MailboxesPage() {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowPasswordModal(null)}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">Şifre Değiştir</h2>
|
||||
<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> için yeni şifre
|
||||
<strong style={{ color: "var(--text-primary)" }}>{showPasswordModal}</strong> {dict.mailboxes.newPasswordFor || "için yeni şifre"}
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Yeni Şifre</label>
|
||||
<input type="password" className="input" placeholder="Yeni şifre"
|
||||
<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)}>İptal</button>
|
||||
<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 />} Güncelle
|
||||
{isPending ? <span className="spinner" /> : <KeyIcon />} {dict.mailboxes.update || "Güncelle"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -379,49 +381,49 @@ export default function MailboxesPage() {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && setShowInfoModal(null)}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">İstemci Bağlantı Bilgileri</h2>
|
||||
<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> hesabını Apple Mail, Outlook veya telefonunuza kurmak için aşağıdaki bilgileri kullanın:
|
||||
<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 }}>IMAP (Gelen Sunucu)</div>
|
||||
<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>Sunucu: <strong>mail.ayris.tech</strong></span>
|
||||
<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 /> Kopyala</button>
|
||||
<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 }}>Port: <strong>993</strong> (SSL/TLS)</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 }}>SMTP (Giden Sunucu)</div>
|
||||
<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>Sunucu: <strong>mail.ayris.tech</strong></span>
|
||||
<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 /> Kopyala</button>
|
||||
<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 }}>Port: <strong>587</strong> (STARTTLS) <span style={{ color: "var(--text-secondary)" }}>veya 465 (SSL)</span></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 }}>Kimlik Doğrulama</div>
|
||||
<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>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 /> Kopyala</button>
|
||||
<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 }}>Şifre: <span style={{ fontStyle: "italic" }}>Hesap oluştururken belirlediğiniz şifre</span></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)}>Tamam</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => setShowInfoModal(null)}>{dict.mailboxes.ok || "Tamam"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Dashboard</h1>
|
||||
<p className="page-subtitle">Hoş geldiniz, {session?.user?.name} 👋</p>
|
||||
<h1 className="page-title">{dict.dashboard.title || "Dashboard"}</h1>
|
||||
<p className="page-subtitle">{dict.dashboard.welcome || "Hoş geldiniz"}, {session?.user?.name} 👋</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="stats-grid">
|
||||
<StatCard
|
||||
label="Toplam Domain"
|
||||
label={dict.dashboard.totalDomains || "Toplam Domain"}
|
||||
value={visibleDomains.length}
|
||||
color="var(--accent)"
|
||||
icon={<GlobeIcon />}
|
||||
/>
|
||||
<StatCard
|
||||
label="Mail Kutuları"
|
||||
label={dict.dashboard.mailboxes || "Mail Kutuları"}
|
||||
value={totalMailboxes}
|
||||
color="var(--success)"
|
||||
icon={<MailIcon />}
|
||||
/>
|
||||
<StatCard
|
||||
label="Alias"
|
||||
label={dict.dashboard.aliases || "Alias"}
|
||||
value={totalAliases}
|
||||
color="var(--warning)"
|
||||
icon={<AtIcon />}
|
||||
/>
|
||||
{role === "SUPER_ADMIN" && (
|
||||
<StatCard
|
||||
label="Tanımlı Kullanıcı"
|
||||
label={dict.dashboard.users || "Tanımlı Kullanıcı"}
|
||||
value={"—"}
|
||||
sub="Kullanıcılar .env'den yönetilir"
|
||||
sub={dict.dashboard.usersSub || "Kullanıcılar .env'den yönetilir"}
|
||||
color="var(--text-muted)"
|
||||
icon={<UsersIcon />}
|
||||
/>
|
||||
@@ -59,17 +68,17 @@ export default async function DashboardPage() {
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border)" }}>
|
||||
<h2 style={{ fontSize: 14, fontWeight: 700, color: "var(--text-primary)" }}>
|
||||
Domain Durumu
|
||||
{dict.dashboard.domainStatus || "Domain Durumu"}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="table-wrap" style={{ border: "none", borderRadius: 0 }}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Mail Kutuları</th>
|
||||
<th>Kota Kullanımı</th>
|
||||
<th>Durum</th>
|
||||
<th>{dict.dashboard.domain || "Domain"}</th>
|
||||
<th>{dict.dashboard.mailboxes || "Mail Kutuları"}</th>
|
||||
<th>{dict.dashboard.quotaUsage || "Kota Kullanımı"}</th>
|
||||
<th>{dict.dashboard.status || "Durum"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -104,7 +113,7 @@ export default async function DashboardPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${String(d.active) === "1" ? "badge-green" : "badge-red"}`}>
|
||||
{String(d.active) === "1" ? "● Aktif" : "● Pasif"}
|
||||
{String(d.active) === "1" ? `● ${dict.dashboard.active || "Aktif"}` : `● ${dict.dashboard.inactive || "Pasif"}`}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,31 +128,31 @@ export default async function DashboardPage() {
|
||||
{/* Quick actions */}
|
||||
<div className="card">
|
||||
<h2 style={{ fontSize: 14, fontWeight: 700, marginBottom: 16, color: "var(--text-primary)" }}>
|
||||
Hızlı İşlemler
|
||||
{dict.dashboard.quickActions || "Hızlı İşlemler"}
|
||||
</h2>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{role === "SUPER_ADMIN" && (
|
||||
<QuickItem
|
||||
href="/dashboard/domains"
|
||||
href={`/${lang}/dashboard/domains`}
|
||||
icon={<GlobeIcon />}
|
||||
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)"
|
||||
/>
|
||||
)}
|
||||
<QuickItem
|
||||
href="/dashboard/mailboxes"
|
||||
href={`/${lang}/dashboard/mailboxes`}
|
||||
icon={<MailIcon />}
|
||||
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" && (
|
||||
<QuickItem
|
||||
href="/dashboard/users"
|
||||
href={`/${lang}/dashboard/users`}
|
||||
icon={<UsersIcon />}
|
||||
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)"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
const dict = useDictionary();
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/users")
|
||||
@@ -35,8 +37,8 @@ export default function UsersPage() {
|
||||
<>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Kullanıcılar</h1>
|
||||
<p className="page-subtitle">Panel kullanıcıları .env dosyasından yönetilir</p>
|
||||
<h1 className="page-title">{dict.users.title || "Kullanıcılar"}</h1>
|
||||
<p className="page-subtitle">{dict.users.subtitle || "Panel kullanıcıları .env dosyasından yönetilir"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,12 +49,16 @@ export default function UsersPage() {
|
||||
<InfoIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: "var(--accent-hover)", marginBottom: 6 }}>Kullanıcı yönetimi hakkında</div>
|
||||
<div style={{ fontWeight: 600, color: "var(--accent-hover)", marginBottom: 6 }}>{dict.users.info ? "Info" : "Kullanıcı yönetimi hakkında"}</div>
|
||||
<div style={{ fontSize: 13, color: "var(--text-secondary)", lineHeight: 1.7 }}>
|
||||
Kullanıcılar <code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.env</code> dosyasındaki{" "}
|
||||
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_0_*</code>,{" "}
|
||||
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_1_*</code>… 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 <code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>.env</code> dosyasındaki{" "}
|
||||
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_0_*</code>,{" "}
|
||||
<code style={{ background: "var(--bg)", padding: "1px 6px", borderRadius: 4, fontSize: 12 }}>USER_1_*</code>… değişkenleriyle tanımlanır.
|
||||
Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: 12, padding: "10px 14px", background: "var(--bg)", borderRadius: "var(--radius)", fontSize: 12, fontFamily: "monospace", color: "var(--text-secondary)", lineHeight: 2 }}>
|
||||
USER_2_NAME="Ahmet Yılmaz"<br />
|
||||
@@ -70,7 +76,7 @@ export default function UsersPage() {
|
||||
<input
|
||||
type="text"
|
||||
className="input search-input"
|
||||
placeholder="İsim veya e-posta ara..."
|
||||
placeholder={dict.users.searchPlaceholder || "İsim veya e-posta ara..."}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
@@ -85,15 +91,15 @@ export default function UsersPage() {
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon"><UsersIcon /></div>
|
||||
<div style={{ fontWeight: 600 }}>Kullanıcı bulunamadı</div>
|
||||
<div style={{ fontWeight: 600 }}>{dict.users.noUsers || "Kullanıcı bulunamadı"}</div>
|
||||
</div>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kullanıcı</th>
|
||||
<th>Rol</th>
|
||||
<th>İzin Verilen Domainler</th>
|
||||
<th>{dict.users.username || "Kullanıcı"}</th>
|
||||
<th>{dict.users.role || "Rol"}</th>
|
||||
<th>{dict.users.domains || "İzin Verilen Domainler"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -112,12 +118,12 @@ export default function UsersPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${u.role === "SUPER_ADMIN" ? "badge-blue" : "badge-green"}`}>
|
||||
{u.role === "SUPER_ADMIN" ? "★ Süper Admin" : "Domain Admin"}
|
||||
{u.role === "SUPER_ADMIN" ? `★ ${dict.users.superAdmin || "Süper Admin"}` : (dict.users.domainAdmin || "Domain Admin")}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{u.domains.includes("*") ? (
|
||||
<span className="badge badge-blue">Tüm domainler</span>
|
||||
<span className="badge badge-blue">{dict.users.allDomains || "Tüm domainler"}</span>
|
||||
) : (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{u.domains.map((d) => (
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user