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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
61
app/api/webhooks/mail/route.ts
Normal file
61
app/api/webhooks/mail/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
27
components/DictionaryContext.tsx
Normal file
27
components/DictionaryContext.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext } from "react";
|
||||
|
||||
const DictionaryContext = createContext<any>(null);
|
||||
|
||||
export function DictionaryProvider({
|
||||
dictionary,
|
||||
children,
|
||||
}: {
|
||||
dictionary: any;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<DictionaryContext.Provider value={dictionary}>
|
||||
{children}
|
||||
</DictionaryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDictionary() {
|
||||
const context = useContext(DictionaryContext);
|
||||
if (!context) {
|
||||
throw new Error("useDictionary must be used within a DictionaryProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
101
components/LanguageSwitcher.tsx
Normal file
101
components/LanguageSwitcher.tsx
Normal file
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="lang-switcher" ref={dropdownRef} style={{ position: "relative", marginBottom: "12px" }}>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{ width: "100%", justifyContent: "space-between", padding: "8px 12px", border: "1px solid var(--border-color)" }}
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<span>{current.flag}</span>
|
||||
<span style={{ fontSize: "13px" }}>{current.label}</span>
|
||||
</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: isOpen ? "rotate(180deg)" : "none", transition: "transform 0.2s" }}>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: "100%",
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginBottom: "4px",
|
||||
background: "var(--card-bg)",
|
||||
border: "1px solid var(--border-color)",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
|
||||
zIndex: 100,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
className={`lang-option ${lang.code === currentLang ? "active" : ""}`}
|
||||
onClick={() => handleLangChange(lang.code)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
background: lang.code === currentLang ? "var(--bg-secondary)" : "transparent",
|
||||
border: "none",
|
||||
color: "var(--text-primary)",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
fontSize: "13px",
|
||||
transition: "background 0.2s"
|
||||
}}
|
||||
>
|
||||
<span>{lang.flag}</span>
|
||||
<span>{lang.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 }) {
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<LanguageSwitcher currentLang={lang} />
|
||||
|
||||
<div className="user-info" style={{ marginBottom: "12px" }}>
|
||||
<div className="user-avatar">{name[0]?.toUpperCase()}</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="user-name" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</div>
|
||||
<div className="user-role">{role === "SUPER_ADMIN" ? "Süper Admin" : "Domain Admin"}</div>
|
||||
<div className="user-role">{role === "SUPER_ADMIN" ? (dict.superAdmin || "Süper Admin") : (dict.domainAdmin || "Domain Admin")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
style={{ width: "100%", justifyContent: "center", fontSize: "12px" }}
|
||||
onClick={async () => {
|
||||
await fetch("/api/mail/auth", { method: "DELETE" });
|
||||
await signOut({ redirect: false });
|
||||
window.location.href = `/${lang}/login`;
|
||||
}}
|
||||
>
|
||||
<LogOutIcon />
|
||||
{dict.logout || "Çıkış Yap"}
|
||||
{dict.sidebar?.logout || "Çıkış Yap"}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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 }: {
|
||||
<div className="modal-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal" style={{ maxWidth: 620 }}>
|
||||
<div className="modal-header">
|
||||
<h2 className="modal-title">{replyTo ? "Yanıtla" : "Yeni Mail"}</h2>
|
||||
<h2 className="modal-title">{replyTo ? (dict.mailClient.reply || "Yanıtla") : (dict.mailClient.composeTitle || "Yeni Mail")}</h2>
|
||||
<button className="modal-close" onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<form onSubmit={handleSend}>
|
||||
<div className="modal-body form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">Alıcı</label>
|
||||
<label className="label">{dict.mailClient.to || "Alıcı"}</label>
|
||||
<input type="email" className="input" placeholder="alici@domain.com" value={to}
|
||||
onChange={(e) => setTo(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">CC (isteğe bağlı)</label>
|
||||
<label className="label">{dict.mailClient.cc || "CC"}</label>
|
||||
<input type="text" className="input" placeholder="cc@domain.com" value={cc}
|
||||
onChange={(e) => setCc(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Konu</label>
|
||||
<label className="label">{dict.mailClient.subject || "Konu"}</label>
|
||||
<input type="text" className="input" value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)} required />
|
||||
</div>
|
||||
@@ -131,7 +133,7 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }}
|
||||
/>
|
||||
<span style={{ fontSize: 20 }}>📎</span>
|
||||
<span>Dosya sürükleyin veya tıklayın</span>
|
||||
<span>{dict.mailClient.dropFiles || "Dosya sürükleyin veya tıklayın"}</span>
|
||||
</div>
|
||||
|
||||
{/* Attachment list */}
|
||||
@@ -145,15 +147,15 @@ export default function ComposeModal({ replyTo, onClose, onSent }: {
|
||||
</div>
|
||||
))}
|
||||
<div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4 }}>
|
||||
Toplam: {formatBytes(totalSize)} — {attachments.length} dosya
|
||||
{formatBytes(totalSize)} — {attachments.length} {dict.mailClient.attachments || "dosya"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>İptal</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>{dict.mailClient.cancel || "İptal"}</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={sending}>
|
||||
{sending ? <span className="spinner" /> : <SendIcon />} Gönder
|
||||
{sending ? <span className="spinner" /> : <SendIcon />} {dict.mailClient.send || "Gönder"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import type { MailFolder } from "@/app/[lang]/dashboard/mail/page";
|
||||
import { useDictionary } from "@/components/DictionaryContext";
|
||||
|
||||
const FOLDER_ICONS: Record<string, string> = {
|
||||
"\\Inbox": "📥",
|
||||
@@ -10,15 +11,6 @@ const FOLDER_ICONS: Record<string, string> = {
|
||||
"\\Archive": "📦",
|
||||
};
|
||||
|
||||
const FOLDER_LABELS: Record<string, string> = {
|
||||
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;
|
||||
|
||||
@@ -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) =>
|
||||
<div className="card" style={{ maxWidth: 420, width: "100%" }}>
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8 }}>📧</div>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: "var(--text-primary)" }}>Mail Hesabına Bağlan</h2>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: "var(--text-primary)" }}>{dict.mailClient.loginTitle || "IMAP Girişi"}</h2>
|
||||
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginTop: 4 }}>
|
||||
Mailcow mail hesabınızın bilgilerini girin
|
||||
{dict.mailClient.loginSubtitle || "Mail kutunuza bağlanın"}
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="form-group">
|
||||
{error && <div className="error-msg">{error}</div>}
|
||||
<div>
|
||||
<label className="label">E-posta Adresi</label>
|
||||
<input type="email" className="input" placeholder="info@domain.com" value={email}
|
||||
<label className="label">{dict.mailClient.emailLabel || "E-posta"}</label>
|
||||
<input type="email" className="input" placeholder={dict.mailClient.emailPlaceholder || "isim@domain.com"} value={email}
|
||||
onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Şifre</label>
|
||||
<input type="password" className="input" placeholder="Mail hesabı şifresi" value={password}
|
||||
<label className="label">{dict.mailClient.passwordLabel || "Şifre"}</label>
|
||||
<input type="password" className="input" placeholder={dict.mailClient.passwordPlaceholder || "********"} value={password}
|
||||
onChange={(e) => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" style={{ width: "100%" }} disabled={loading}>
|
||||
{loading ? <span className="spinner" /> : "Bağlan"}
|
||||
{loading ? <span className="spinner" /> : (dict.mailClient.connect || "Bağlan")}
|
||||
</button>
|
||||
<p style={{ fontSize: 11, color: "var(--text-muted)", textAlign: "center", marginTop: 8 }}>
|
||||
Şifreniz sunucuda saklanmaz, sadece oturum süresince kullanılır.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 <div className="empty-state" style={{ padding: 40 }}><span className="spinner" style={{ width: 20, height: 20 }} /></div>;
|
||||
}
|
||||
@@ -34,7 +40,7 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="empty-state" style={{ padding: 40 }}>
|
||||
<div style={{ fontSize: 13, color: "var(--text-muted)" }}>Bu klasörde mail yok</div>
|
||||
<div style={{ fontSize: 13, color: "var(--text-muted)" }}>{dict.mailClient.emptyFolder || "Bu klasör boş"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,11 +59,11 @@ export default function MessageList({ messages, loading, selectedUid, onSelect,
|
||||
<div className="message-content">
|
||||
<div className="message-top">
|
||||
<span className="message-sender">{senderName(m)}</span>
|
||||
<span className="message-time">{timeAgo(m.date)}</span>
|
||||
<span className="message-time">{timeAgo(m.date, lang)}</span>
|
||||
</div>
|
||||
<div className="message-subject">{m.subject}</div>
|
||||
<div className="message-subject">{m.subject || dict.mailClient.noSubject || "(Konu Yok)"}</div>
|
||||
</div>
|
||||
{m.hasAttachments && <span className="message-attach" title="Ek var">📎</span>}
|
||||
{m.hasAttachments && <span className="message-attach" title={dict.mailClient.attachments || "Ekler"}>📎</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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 }: {
|
||||
<div className="message-view">
|
||||
{/* Header */}
|
||||
<div className="message-view-header">
|
||||
<h2 className="message-view-subject">{message.subject}</h2>
|
||||
<h2 className="message-view-subject">{message.subject || dict.mailClient.noSubject || "(Konu Yok)"}</h2>
|
||||
<div className="message-view-actions">
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title="Yanıtla">
|
||||
<ReplyIcon /> Yanıtla
|
||||
<button className="btn btn-ghost btn-sm" onClick={onReply} title={dict.mailClient.reply || "Yanıtla"}>
|
||||
<ReplyIcon /> {dict.mailClient.reply || "Yanıtla"}
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title="Sil">
|
||||
<TrashIcon /> Sil
|
||||
<button className="btn btn-danger btn-sm" onClick={onDelete} title={dict.mailClient.delete || "Sil"}>
|
||||
<TrashIcon /> {dict.mailClient.delete || "Sil"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,7 +81,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
{message.attachments.length > 0 && (
|
||||
<div className="message-attachments">
|
||||
<div style={{ width: "100%", fontSize: 12, fontWeight: 600, color: "var(--text-secondary)", marginBottom: 6 }}>
|
||||
📎 {message.attachments.length} ek
|
||||
📎 {message.attachments.length} {dict.mailClient.attachments || "ek"}
|
||||
</div>
|
||||
{message.attachments.map((att, i) => (
|
||||
<div key={i} className="attachment-chip" onClick={() => handleAttachment(att, false)}>
|
||||
@@ -89,7 +94,7 @@ export default function MessageView({ message, onReply, onDelete, folder }: {
|
||||
<button
|
||||
className="att-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleAttachment(att, false); }}
|
||||
title="İndir"
|
||||
title={dict.mailClient.download || "İndir"}
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
|
||||
@@ -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<void> {
|
||||
export async function setMailSession(data: Omit<MailSessionData, "ownerId">): Promise<void> {
|
||||
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<void> {
|
||||
|
||||
/** Get mail credentials from cookie */
|
||||
export async function getMailSession(): Promise<MailSessionData | null> {
|
||||
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 */
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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++;
|
||||
|
||||
Reference in New Issue
Block a user