diff --git a/app/[lang]/dashboard/logs/page.tsx b/app/[lang]/dashboard/logs/page.tsx new file mode 100644 index 0000000..44609b9 --- /dev/null +++ b/app/[lang]/dashboard/logs/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useDictionary } from "@/components/DictionaryContext"; + +interface Log { + id: string; + mailbox: string; + sender: string | null; + subject: string | null; + status: string; + error: string | null; + createdAt: string; + user?: { + name: string | null; + email: string; + } | null; +} + +export default function LogsPage() { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const dict = useDictionary(); + + const fetchLogs = async () => { + setLoading(true); + try { + const res = await fetch("/api/logs"); + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP error! status: ${res.status}`); + } + const data = await res.json(); + if (Array.isArray(data)) setLogs(data); + } catch (error: any) { + console.error("Failed to fetch logs:", error); + alert(error.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchLogs(); + }, []); + + return ( + <> +
+
+

{dict.logs?.title || "Bildirim Logları"}

+

{dict.logs?.subtitle || "Son gönderilen bildirimlerin durumu"}

+
+ +
+ +
+
+ {loading ? ( +
+ +
+ ) : logs.length === 0 ? ( +
+
{dict.logs?.noLogs || "Log kaydı bulunamadı"}
+
+ ) : ( + + + + + + + + + + + {logs.map((log) => ( + + + + + + + ))} + +
{dict.logs?.mailbox || "Alıcı"}{dict.logs?.sender || "Gönderen"} / {dict.logs?.subject || "Konu"}{dict.logs?.status || "Durum"}{dict.logs?.date || "Tarih"}
+
{log.mailbox}
+ {log.user && ( +
+ → {log.user.name || log.user.email} +
+ )} +
+
{log.sender || "Unknown"}
+
+ {log.subject || "(No Subject)"} +
+
+
+ + {log.status === "SENT" ? (dict.logs?.sent || "GÖNDERİLDİ") : (dict.logs?.failed || "HATA")} + + {log.error && ( +
+ {log.error} +
+ )} +
+
+ {new Date(log.createdAt).toLocaleString()} +
+ )} +
+
+ + ); +} + +function RefreshIcon() { + return ( + + + + + + + ); +} diff --git a/app/[lang]/dashboard/mailboxes/page.tsx b/app/[lang]/dashboard/mailboxes/page.tsx index ca98ed9..dd4ed67 100644 --- a/app/[lang]/dashboard/mailboxes/page.tsx +++ b/app/[lang]/dashboard/mailboxes/page.tsx @@ -19,6 +19,13 @@ interface Domain { domain_name: string; } +interface User { + id: string; + name: string; + email: string; + telegramId?: string; +} + export default function MailboxesPage() { const { data: session } = useSession(); const [domains, setDomains] = useState([]); @@ -30,12 +37,14 @@ export default function MailboxesPage() { const [showInfoModal, setShowInfoModal] = useState(null); const [isPending, startTransition] = useTransition(); const [search, setSearch] = useState(""); - const [createForm, setCreateForm] = useState({ local_part: "", name: "", password: "", quota: 3072 }); + const [users, setUsers] = useState([]); + const [createForm, setCreateForm] = useState({ local_part: "", name: "", password: "", quota: 3072, notifyUserId: "" }); const [newPassword, setNewPassword] = useState(""); const [formError, setFormError] = useState(""); const dict = useDictionary(); useEffect(() => { + // Fetch domains fetch("/api/domains") .then((r) => r.json()) .then((data: Domain[]) => { @@ -44,6 +53,13 @@ export default function MailboxesPage() { setSelectedDomain(data[0].domain_name); } }); + + // Fetch users for mapping selection + fetch("/api/users") + .then((r) => r.json()) + .then((data) => { + if (Array.isArray(data)) setUsers(data); + }); }, []); const fetchMailboxes = useCallback(async (domain: string) => { @@ -63,21 +79,46 @@ export default function MailboxesPage() { e.preventDefault(); setFormError(""); startTransition(async () => { - const res = await fetch("/api/mailboxes", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ...createForm, domain: selectedDomain }), - }); - const data = await res.json(); - if (res.ok) { - setShowCreateModal(false); - setCreateForm({ local_part: "", name: "", password: "", quota: 3072 }); - fetchMailboxes(selectedDomain); - } else { - const msg = Array.isArray(data) - ? data.map((d: { msg?: unknown }) => JSON.stringify(d.msg)).join(", ") - : (data?.error ?? "Mailcow bağlantısını kontrol edin"); - setFormError(String(msg)); + try { + const res = await fetch("/api/mailboxes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...createForm, domain: selectedDomain }), + }); + + let data: any = {}; + try { + data = await res.json(); + } catch (e) { + data = { error: "Sunucudan geçersiz yanıt geldi (JSON hatası)." }; + } + + if (res.ok) { + // If a notification user is selected, create the mapping + if (createForm.notifyUserId) { + const fullEmail = `${createForm.local_part}@${selectedDomain}`; + await fetch("/api/mappings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: fullEmail, + userId: createForm.notifyUserId, + }), + }); + } + + setShowCreateModal(false); + setCreateForm({ local_part: "", name: "", password: "", quota: 3072, notifyUserId: "" }); + fetchMailboxes(selectedDomain); + } else { + const msg = Array.isArray(data) + ? data.map((d: { msg?: unknown }) => JSON.stringify(d.msg)).join(", ") + : (data?.error ?? "Mailcow bağlantısını veya veritabanını kontrol edin"); + setFormError(String(msg)); + } + } catch (error: any) { + console.error("Mailbox creation failed:", error); + setFormError(error.message); } }); }; @@ -333,6 +374,21 @@ export default function MailboxesPage() { setCreateForm({ ...createForm, quota: parseInt(e.target.value) || 3072 })} /> +
+ + +
diff --git a/app/[lang]/dashboard/mappings/page.tsx b/app/[lang]/dashboard/mappings/page.tsx new file mode 100644 index 0000000..2d2df5b --- /dev/null +++ b/app/[lang]/dashboard/mappings/page.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useDictionary } from "@/components/DictionaryContext"; + +interface User { + id: string; + name: string; + email: string; +} + +interface Mapping { + id: string; + email: string; + userId: string; + user: User; +} + +export default function MappingsPage() { + const [mappings, setMappings] = useState([]); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + + // Form state + const [newEmail, setNewEmail] = useState(""); + const [newUserId, setNewUserId] = useState(""); + const [saving, setSaving] = useState(false); + + const dict = useDictionary(); + + const fetchData = async () => { + setLoading(true); + try { + const [mRes, uRes] = await Promise.all([ + fetch("/api/mappings"), + fetch("/api/users") + ]); + const [mData, uData] = await Promise.all([mRes.json(), uRes.json()]); + setMappings(Array.isArray(mData) ? mData : []); + setUsers(Array.isArray(uData) ? uData : []); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleAdd = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + const res = await fetch("/api/mappings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: newEmail, userId: newUserId }) + }); + if (res.ok) { + setIsModalOpen(false); + setNewEmail(""); + setNewUserId(""); + fetchData(); + } + } catch (e) { + console.error(e); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Bu eşleştirmeyi silmek istediğinize emin misiniz?")) return; + try { + const res = await fetch(`/api/mappings/${id}`, { method: "DELETE" }); + if (res.ok) fetchData(); + } catch (e) { + console.error(e); + } + }; + + const filtered = mappings.filter(m => + m.email.toLowerCase().includes(search.toLowerCase()) || + m.user.name.toLowerCase().includes(search.toLowerCase()) + ); + + return ( + <> +
+
+

{dict.mappings.title}

+

{dict.mappings.subtitle}

+
+ +
+ +
+
+
+ + setSearch(e.target.value)} + /> +
+
+ +
+ {loading ? ( +
+ ) : filtered.length === 0 ? ( +
+
+
{dict.mappings.noMappings}
+
+ ) : ( + + + + + + + + + + {filtered.map((m) => ( + + + + + + ))} + +
{dict.mappings.email}{dict.mappings.user}
+
{m.email}
+
+
+
+ {m.user.name[0]?.toUpperCase()} +
+ {m.user.name} + ({m.user.email}) +
+
+ +
+ )} +
+
+ + {isModalOpen && ( +
+
+
+

{dict.mappings.addMapping}

+ +
+
+
+ + setNewEmail(e.target.value)} + required + /> +
+
+ + +
+
+ + +
+
+
+
+ )} + + ); +} + +function PlusIcon() { return ; } +function SearchIcon() { return ; } +function LinkIcon() { return ; } +function TrashIcon() { return ; } diff --git a/app/[lang]/dashboard/users/page.tsx b/app/[lang]/dashboard/users/page.tsx index eeda6fa..48a3988 100644 --- a/app/[lang]/dashboard/users/page.tsx +++ b/app/[lang]/dashboard/users/page.tsx @@ -9,27 +9,108 @@ interface User { email: string; role: string; domains: string[]; + telegramId?: string; + password?: string; } export default function UsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + + // Form state + const [formData, setFormData] = useState({ + name: "", + email: "", + password: "", + role: "DOMAIN_ADMIN", + domains: "", + telegramId: "" + }); + const [saving, setSaving] = useState(false); + const dict = useDictionary(); + const fetchUsers = async () => { + setLoading(true); + try { + const res = await fetch("/api/users"); + const data = await res.json(); + setUsers(Array.isArray(data) ? data : []); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + useEffect(() => { - fetch("/api/users") - .then((r) => r.json()) - .then((data) => { - setUsers(Array.isArray(data) ? data : []); - setLoading(false); - }) - .catch(() => setLoading(false)); + fetchUsers(); }, []); + const openAddModal = () => { + setEditingUser(null); + setFormData({ name: "", email: "", password: "", role: "DOMAIN_ADMIN", domains: "", telegramId: "" }); + setIsModalOpen(true); + }; + + const openEditModal = (user: User) => { + setEditingUser(user); + setFormData({ + name: user.name || "", + email: user.email, + password: user.password || "", + role: user.role, + domains: user.domains.join(", "), + telegramId: user.telegramId || "" + }); + setIsModalOpen(true); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + const url = editingUser ? `/api/users/${editingUser.id}` : "/api/users"; + const method = editingUser ? "PATCH" : "POST"; + + const payload = { + ...formData, + domains: formData.domains.split(",").map(d => d.trim()).filter(Boolean) + }; + + const res = await fetch(url, { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + setIsModalOpen(false); + fetchUsers(); + } + } catch (e) { + console.error(e); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Bu kullanıcıyı silmek istediğinize emin misiniz?")) return; + try { + const res = await fetch(`/api/users/${id}`, { method: "DELETE" }); + if (res.ok) fetchUsers(); + } catch (e) { + console.error(e); + } + }; + const filtered = users.filter( (u) => - u.name.toLowerCase().includes(search.toLowerCase()) || + u.name?.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()) ); @@ -37,35 +118,24 @@ export default function UsersPage() { <>
-

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

-

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

+

{dict.users.title}

+

{dict.users.subtitle}

+
- {/* Info card */}
-
{dict.users.info ? "Info" : "Kullanıcı yönetimi hakkında"}
+
Kullanıcı Yönetimi
- {dict.users.info || ( - <> - Kullanıcılar .env dosyasındaki{" "} - USER_0_*,{" "} - USER_1_*… değişkenleriyle tanımlanır. - Yeni kullanıcı eklemek için .env dosyasını düzenleyip uygulamayı yeniden başlatın. - - )} -
-
- USER_2_NAME="Ahmet Yılmaz"
- USER_2_EMAIL="ahmet@ayristech.com"
- USER_2_PASSWORD="güçlü-şifre"
- USER_2_ROLE="DOMAIN_ADMIN"
- USER_2_DOMAINS="yenidomain.com" + {dict.users.info}
@@ -76,7 +146,7 @@ export default function UsersPage() { setSearch(e.target.value)} /> @@ -85,21 +155,21 @@ export default function UsersPage() {
{loading ? ( -
- -
+
) : filtered.length === 0 ? (
-
{dict.users.noUsers || "Kullanıcı bulunamadı"}
+
{dict.users.noUsers}
) : ( - - - + + + + + @@ -108,7 +178,7 @@ export default function UsersPage() { + + ))} @@ -139,10 +218,63 @@ export default function UsersPage() { )} + + {isModalOpen && ( +
+
+
+

{editingUser ? dict.users.editUser : dict.users.addUser}

+ +
+
+
+
+ + setFormData({...formData, name: e.target.value})} required /> +
+
+ + setFormData({...formData, email: e.target.value})} required /> +
+
+
+ + setFormData({...formData, password: e.target.value})} required={!editingUser} /> +
+
+
+ + +
+
+ + setFormData({...formData, telegramId: e.target.value})} /> +
+
+
+ + setFormData({...formData, domains: e.target.value})} /> +
+
+ + +
+ +
+
+ )} ); } +function PlusIcon() { return ; } function SearchIcon() { return ; } -function UsersIcon() { return ; } +function UsersIcon() { return ; } function InfoIcon() { return ; } +function TrashIcon() { return ; } +function EditIcon() { return ; } diff --git a/app/api/logs/route.ts b/app/api/logs/route.ts new file mode 100644 index 0000000..62e889a --- /dev/null +++ b/app/api/logs/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; + +// GET /api/logs — list notification logs +export async function GET() { + const session = await auth(); + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const logs = await prisma.notificationLog.findMany({ + include: { user: true }, + orderBy: { createdAt: "desc" }, + take: 100, // Last 100 logs + }); + + return NextResponse.json(logs); + } catch (error: any) { + console.error("[API Logs] Error:", error.message); + return NextResponse.json({ error: "Tablo bulunamadı veya veritabanı hatası. Migration yapıldığından emin olun." }, { status: 500 }); + } +} diff --git a/app/api/mailboxes/route.ts b/app/api/mailboxes/route.ts index c4e068a..64863c7 100644 --- a/app/api/mailboxes/route.ts +++ b/app/api/mailboxes/route.ts @@ -1,10 +1,12 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/auth"; -import { getMailboxes, createMailbox } from "@/lib/mailcow"; +import { getMailboxes, createMailbox, setupMailboxForwarding } from "@/lib/mailcow"; import { canAccessDomain } from "@/lib/users"; +import { prisma } from "@/lib/prisma"; // GET /api/mailboxes?domain=example.com export async function GET(req: NextRequest) { + // ... existing GET ... const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -26,6 +28,7 @@ export async function POST(req: NextRequest) { const body = await req.json(); const { local_part, domain, name, password, quota } = body; + const fullEmail = `${local_part}@${domain}`; if (!local_part || !domain || !name || !password) { return NextResponse.json({ error: "Eksik alan" }, { status: 400 }); @@ -35,6 +38,62 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Bu domaine erişim yetkiniz yok" }, { status: 403 }); } + // 1. Create Mailbox in Mailcow const result = await createMailbox({ local_part, domain, name, password, quota }); - return NextResponse.json(result.data, { status: result.ok ? 200 : 502 }); + + if (!result.ok) { + // Log failure to SystemLog + try { + await prisma.systemLog.create({ + data: { + level: "ERROR", + message: `Mailbox creation failed: ${fullEmail}`, + details: JSON.stringify(result.data), + }, + }); + } catch (e) { + console.error("[SystemLog] Failed to log error", e); + } + return NextResponse.json(result.data, { status: 502 }); + } + + // 2. Automated "One-Click" Setup: Create Forwarding to Webhook + const webhookUrl = `${req.nextUrl.origin}/api/webhooks/mail`; + console.log(`[Setup] Setting up auto-forwarding for ${fullEmail} to ${webhookUrl}`); + + const setupResult = await setupMailboxForwarding(fullEmail, webhookUrl); + + if (!setupResult.ok) { + console.error(`[Setup] Failed to setup auto-forwarding for ${fullEmail}`); + try { + await prisma.systemLog.create({ + data: { + level: "WARN", + message: `Auto-forwarding setup failed for ${fullEmail}`, + details: JSON.stringify(setupResult.data), + }, + }); + } catch (e) { + console.error("[SystemLog] Failed to log warning", e); + } + // We still return success for mailbox creation, but maybe with a warning header/prop + return NextResponse.json({ + ...result.data, + setup_warning: "Bildirim kurulumu otomatik yapılamadı, lütfen manuel kontrol edin." + }); + } + + // Log success + try { + await prisma.systemLog.create({ + data: { + level: "INFO", + message: `Mailbox created and notification setup completed: ${fullEmail}`, + }, + }); + } catch (e) { + console.error("[SystemLog] Failed to log success", e); + } + + return NextResponse.json(result.data); } diff --git a/app/api/mappings/[id]/route.ts b/app/api/mappings/[id]/route.ts new file mode 100644 index 0000000..5f0063e --- /dev/null +++ b/app/api/mappings/[id]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; + +// DELETE /api/mappings/[id] — delete a mapping +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + const { id } = await params; + + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + await prisma.mailboxMapping.delete({ + where: { id }, + }); + + return NextResponse.json({ status: "ok" }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/mappings/route.ts b/app/api/mappings/route.ts new file mode 100644 index 0000000..72d3e65 --- /dev/null +++ b/app/api/mappings/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; + +// GET /api/mappings — list all mappings +export async function GET() { + const session = await auth(); + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const mappings = await prisma.mailboxMapping.findMany({ + include: { user: true }, + orderBy: { createdAt: "desc" }, + }); + + return NextResponse.json(mappings); +} + +// POST /api/mappings — create a new mapping +export async function POST(req: NextRequest) { + const session = await auth(); + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const body = await req.json(); + const { email, userId } = body; + + const mapping = await prisma.mailboxMapping.create({ + data: { + email: email.toLowerCase().trim(), + userId, + }, + include: { user: true }, + }); + + return NextResponse.json(mapping); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/users/[id]/route.ts b/app/api/users/[id]/route.ts new file mode 100644 index 0000000..a5ebfc8 --- /dev/null +++ b/app/api/users/[id]/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; + +// PATCH /api/users/[id] — update a user +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + const { id } = await params; + + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const body = await req.json(); + const { name, email, password, role, domains, telegramId } = body; + + const user = await prisma.user.update({ + where: { id }, + data: { + name, + email: email?.toLowerCase(), + password, + role, + domains, + telegramId, + }, + }); + + return NextResponse.json(user); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +// DELETE /api/users/[id] — delete a user +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + const { id } = await params; + + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + await prisma.user.delete({ + where: { id }, + }); + + return NextResponse.json({ status: "ok" }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 0376fa6..a8df5e4 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -1,22 +1,45 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/auth"; -import { getUsers } from "@/lib/users"; +import { prisma } from "@/lib/prisma"; -// GET /api/users — super admin only, lists env-defined users (no passwords) +// GET /api/users — list all users export async function GET() { const session = await auth(); if (!session || session.user.role !== "SUPER_ADMIN") { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } - const allUsers = await getUsers(); - const users = allUsers.map(({ id, name, email, role, domains }) => ({ - id, - name, - email, - role, - domains, - })); + const users = await prisma.user.findMany({ + orderBy: { createdAt: "asc" }, + }); return NextResponse.json(users); } + +// POST /api/users — create a new user +export async function POST(req: NextRequest) { + const session = await auth(); + if (!session || session.user.role !== "SUPER_ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + try { + const body = await req.json(); + const { name, email, password, role, domains, telegramId } = body; + + const user = await prisma.user.create({ + data: { + name, + email: email.toLowerCase(), + password, + role: role || "DOMAIN_ADMIN", + domains: domains || [], + telegramId, + }, + }); + + return NextResponse.json(user); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/api/webhooks/mail/route.ts b/app/api/webhooks/mail/route.ts index 4d38e79..217861d 100644 --- a/app/api/webhooks/mail/route.ts +++ b/app/api/webhooks/mail/route.ts @@ -9,15 +9,20 @@ import { prisma } from "@/lib/prisma"; */ export async function POST(req: NextRequest) { + let aliciMail = "Bilinmiyor"; + let sender = "Bilinmiyor"; + let subject = "(Konu Yok)"; + try { const data = await req.json(); + console.log("[Mail Webhook] Gelen Payload:", JSON.stringify(data)); - // Extract basic info from the incoming payload - const aliciMail = (data.to || data.rcpt || "").toLowerCase().trim(); - const sender = data.from || "Bilinmiyor"; - const subject = data.subject || "(Konu Yok)"; + // Extract basic info from the incoming payload (Mailcow handles these fields) + aliciMail = (data.to || data.rcpt || "").toLowerCase().trim(); + sender = data.from || "Bilinmiyor"; + subject = data.subject || "(Konu Yok)"; - console.log(`[Mail Webhook] Yeni mail geldi: ${sender} -> ${aliciMail}`); + console.log(`[Mail Webhook] İşleniyor: ${sender} -> ${aliciMail}`); // 1. Find mapping in database const mapping = await prisma.mailboxMapping.findUnique({ @@ -31,33 +36,89 @@ export async function POST(req: NextRequest) { if (targetChatId && process.env.TELEGRAM_BOT_TOKEN) { const message = `🔔 *Yeni Mail Geldi!*\n\n📧 *Alıcı:* ${aliciMail}\n👤 *Gönderen:* ${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", - }), - }); + let status = "SENT"; + let errorDetail = null; - if (!res.ok) { - const errorText = await res.text(); - console.error(`[Mail Webhook] Telegram API hatası: ${res.status} ${errorText}`); - } else { - console.log(`[Webhook] Bildirim ${user.email} kullanıcısına (ID: ${targetChatId}) gönderildi.`); + try { + 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) { + status = "FAILED"; + errorDetail = `Telegram API Error: ${res.status} ${await res.text()}`; + } + } catch (err: any) { + status = "FAILED"; + errorDetail = `Network Error: ${err.message}`; } + + // Log successful/failed delivery + await prisma.notificationLog.create({ + data: { + mailbox: aliciMail, + sender, + subject, + status, + error: errorDetail, + userId: user.id, + }, + }); + } else { + // Log that user was found but notification skipped + await prisma.notificationLog.create({ + data: { + mailbox: aliciMail, + sender, + subject, + status: "FAILED", + error: !process.env.TELEGRAM_BOT_TOKEN ? "Bot token missing" : "User has no Telegram ID", + userId: user.id, + }, + }); } } else { console.log(`[Webhook] Sahibi bilinmeyen veya eşleşmeyen mail: ${aliciMail}`); + + // Log unmapped mail + await prisma.notificationLog.create({ + data: { + mailbox: aliciMail, + sender, + subject, + status: "FAILED", + error: "No user mapping found for this email", + }, + }); } return NextResponse.json({ status: "ok" }); } catch (error: any) { console.error(`[Mail Webhook] Hata: ${error.message}`); + + // Attempt to log the fatal error if we have enough info + try { + await prisma.notificationLog.create({ + data: { + mailbox: aliciMail, + sender, + subject, + status: "FAILED", + error: `Fatal Error: ${error.message}`, + }, + }); + } catch (dbErr) { + console.error("[Mail Webhook] Could not even log the error to DB"); + } + return NextResponse.json({ error: "İşlem başarısız" }, { status: 500 }); } } diff --git a/app/dictionaries/en.json b/app/dictionaries/en.json index e0deba9..7528958 100644 --- a/app/dictionaries/en.json +++ b/app/dictionaries/en.json @@ -14,6 +14,8 @@ "mailboxes": "Mailboxes", "mailClient": "Mail Client", "users": "User Management", + "mappings": "Mappings", + "logs": "Notification Logs", "logout": "Log Out", "general": "GENERAL", "management": "MANAGEMENT", @@ -27,7 +29,7 @@ "mailboxes": "Mailboxes", "aliases": "Aliases", "users": "Defined Users", - "usersSub": "Users are managed from .env", + "usersSub": "Users managed via database", "domainStatus": "Domain Status", "domain": "Domain", "quotaUsage": "Quota Usage", @@ -40,7 +42,7 @@ "manageMailboxes": "Mailboxes", "manageMailboxesDesc": "Create new account, change password, delete", "manageUsers": "Users", - "manageUsersDesc": "View defined panel users from .env" + "manageUsersDesc": "Manage and authorize panel users" }, "domains": { "title": "Domains", @@ -61,16 +63,41 @@ "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", + "title": "User Management", + "subtitle": "Manage panel access permissions and notification settings", + "addUser": "Add User", + "editUser": "Edit User", + "username": "User", "role": "Role", - "domains": "Authorized Domains", + "domains": "Allowed Domains", "superAdmin": "Super Admin", "domainAdmin": "Domain Admin", - "allDomains": "All Domains" + "allDomains": "All domains", + "noUsers": "No users found", + "searchPlaceholder": "Search name or email...", + "info": "Users are now stored in the database. You can update permissions and notification settings here." + }, + "mappings": { + "title": "Mail Mappings", + "subtitle": "Manage which user receives notifications for incoming mails", + "addMapping": "Add Mapping", + "email": "Email Address", + "user": "User to Notify", + "noMappings": "No mappings found", + "searchPlaceholder": "Search email address..." + }, + "logs": { + "title": "Notification Logs", + "subtitle": "Status and details of recently sent notifications", + "mailbox": "Recipient Mail", + "sender": "Sender", + "subject": "Subject", + "status": "Status", + "date": "Date", + "error": "Error Detail", + "noLogs": "No log records found yet", + "sent": "SENT", + "failed": "FAILED" }, "mailboxes": { "title": "Mailboxes", @@ -100,6 +127,8 @@ "quotaMb": "Quota (MB)", "cancel": "Cancel", "create": "Create", + "notifyUser": "Notify User (TG)", + "noNotify": "No Notifications", "newPasswordFor": "New password for", "update": "Update", "connectionInfo": "Client Connection Info", diff --git a/app/dictionaries/tr.json b/app/dictionaries/tr.json index f0a23b3..cbb0151 100644 --- a/app/dictionaries/tr.json +++ b/app/dictionaries/tr.json @@ -14,6 +14,8 @@ "mailboxes": "Mail Hesapları", "mailClient": "Mail İstemcisi", "users": "Kullanıcı Yönetimi", + "mappings": "Eşleştirmeler", + "logs": "Bildirim Logları", "logout": "Çıkış Yap", "general": "GENEL", "management": "YÖNETİM", @@ -27,7 +29,7 @@ "mailboxes": "Mail Kutuları", "aliases": "Alias", "users": "Tanımlı Kullanıcı", - "usersSub": "Kullanıcılar .env'den yönetilir", + "usersSub": "Kullanıcılar veritabanından yönetilir", "domainStatus": "Domain Durumu", "domain": "Domain", "quotaUsage": "Kota Kullanımı", @@ -40,7 +42,7 @@ "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" + "manageUsersDesc": "Panel kullanıcılarını yönet ve yetkilendir" }, "domains": { "title": "Domainler", @@ -61,16 +63,41 @@ "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", + "title": "Kullanıcı Yönetimi", + "subtitle": "Panel erişim yetkilerini ve bildirim ayarlarını yönetin", + "addUser": "Yeni Kullanıcı", + "editUser": "Kullanıcıyı Düzenle", + "username": "Kullanıcı", "role": "Rol", - "domains": "Yetkili Domainler", + "domains": "İzin Verilen Domainler", "superAdmin": "Süper Admin", "domainAdmin": "Domain Admin", - "allDomains": "Tüm Domainler" + "allDomains": "Tüm domainler", + "noUsers": "Kullanıcı bulunamadı", + "searchPlaceholder": "İsim veya e-posta ara...", + "info": "Kullanıcılar artık veritabanında saklanmaktadır. Buradan yetki ve bildirim ayarlarını güncelleyebilirsiniz." + }, + "mappings": { + "title": "Mail Eşleştirmeleri", + "subtitle": "Gelen maillerin hangi kullanıcıya bildirileceğini yönetin", + "addMapping": "Yeni Eşleştirme", + "email": "Mail Adresi", + "user": "Bildirilecek Kullanıcı", + "noMappings": "Eşleştirme bulunamadı", + "searchPlaceholder": "Mail adresi ara..." + }, + "logs": { + "title": "Bildirim Logları", + "subtitle": "Son gönderilen bildirimlerin durumu ve detayları", + "mailbox": "Alıcı Mail", + "sender": "Gönderen", + "subject": "Konu", + "status": "Durum", + "date": "Tarih", + "error": "Hata Detayı", + "noLogs": "Henüz log kaydı bulunmuyor", + "sent": "GÖNDERİLDİ", + "failed": "HATA" }, "mailboxes": { "title": "Mail Hesapları", @@ -100,6 +127,8 @@ "quotaMb": "Kota (MB)", "cancel": "İptal", "create": "Oluştur", + "notifyUser": "Bildirim Gidecek Kullanıcı (TG)", + "noNotify": "Bildirim Gönderme", "newPasswordFor": "için yeni şifre", "update": "Güncelle", "connectionInfo": "İstemci Bağlantı Bilgileri", diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index ff81582..d520c83 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -25,7 +25,9 @@ export default function Sidebar({ dict, lang }: { dict: any; lang: string }) { items: [ { 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/mappings`, label: dict.sidebar?.mappings || "Eşleştirmeler", icon: LinkIcon, roles: ["SUPER_ADMIN"] }, { href: `/${lang}/dashboard/mailboxes`, label: dict.sidebar?.mailboxes || "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] }, + { href: `/${lang}/dashboard/logs`, label: dict.sidebar?.logs || "Loglar", icon: ListIcon, roles: ["SUPER_ADMIN"] }, ], }, ]; @@ -153,3 +155,24 @@ function LogOutIcon() { ); } + +function LinkIcon() { + return ( + + + + + ); +} +function ListIcon() { + return ( + + + + + + + + + ); +} diff --git a/lib/mailcow.ts b/lib/mailcow.ts index dd55fdd..e467e1e 100644 --- a/lib/mailcow.ts +++ b/lib/mailcow.ts @@ -28,7 +28,8 @@ async function mfetch(path: string, options: RequestInit = {}) { cache: "no-store", }); if (!res.ok) { - const text = await res.text(); + const clone = res.clone(); + const text = await clone.text(); console.error(`[Mailcow API] Error ${res.status}: ${text}`); } return res; @@ -228,3 +229,23 @@ export async function getDKIM(domain: string) { if (!res.ok) return null; return res.json(); } + +// ─── Forwarding / Webhook Setup ────────────────────────────── +// Automates the "one-click" configuration of mailbox notifications +export async function setupMailboxForwarding(address: string, webhookUrl: string) { + // Check if webhookUrl is localhost and warn in console + if (webhookUrl.includes("localhost")) { + console.warn(`[Setup] WARNING: Using localhost for webhook (${webhookUrl}). Mailcow will not be able to reach this!`); + } + + const body = { + address: address, + goto: webhookUrl, + active: 1, + sogo_visible: 0, + }; + const res = await mfetch("/add/alias", { method: "POST", body: JSON.stringify(body) }); + const data = await res.json(); + return { ok: res.ok, data }; +} + diff --git a/lib/users.ts b/lib/users.ts index 4b1d181..dad56df 100644 --- a/lib/users.ts +++ b/lib/users.ts @@ -1,19 +1,6 @@ /** * lib/users.ts - * Reads user config from environment variables — no database needed. - * - * .env format: - * USER_0_NAME="Mustafa Ayris" - * USER_0_EMAIL="mustafa@ayristech.com" - * USER_0_PASSWORD="mustafa123" - * USER_0_ROLE="SUPER_ADMIN" // or "DOMAIN_ADMIN" - * USER_0_DOMAINS="*" // "*" for all, or "domain1.com,domain2.com" - * - * USER_1_NAME="Emina Karabudak" - * USER_1_EMAIL="emina@ayristech.com" - * USER_1_PASSWORD="emina123" - * USER_1_ROLE="DOMAIN_ADMIN" - * USER_1_DOMAINS="aveminakarabudak.com" + * Manages panel users via PostgreSQL database. */ import { prisma } from "./prisma"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d935ead..d090f50 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { telegramId String? mailboxMappings MailboxMapping[] notificationConfigs NotificationConfig[] + notificationLogs NotificationLog[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -41,3 +42,23 @@ model NotificationConfig { active Boolean @default(true) createdAt DateTime @default(now()) } + +model NotificationLog { + id String @id @default(cuid()) + mailbox String // Bildirim gelen mail adresi + sender String? // Gönderen kişi + subject String? // Konu + status String // "SENT", "FAILED" + error String? // Hata varsa detayı + userId String? // Hangi kullanıcıya bildirim gittiği + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) +} + +model SystemLog { + id String @id @default(cuid()) + level String // "INFO", "WARN", "ERROR" + message String + details String? + createdAt DateTime @default(now()) +} diff --git a/scripts/seed.ts b/scripts/seed.ts index 58a8b34..5b179d9 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -1,56 +1,29 @@ import { prisma } from "../lib/prisma"; -import { getUsers } from "../lib/users"; import "dotenv/config"; async function main() { console.log("Seeding database..."); - // 1. Migrate Users - const users = await getUsers(); - for (const user of users) { - console.log(`Migrating user: ${user.email}`); - await prisma.user.upsert({ - where: { email: user.email }, - update: { - name: user.name, - password: user.password, - role: user.role, - domains: user.domains, - telegramId: user.telegramId, - }, - create: { - email: user.email, - name: user.name, - password: user.password, - role: user.role, - domains: user.domains, - telegramId: user.telegramId, + // Create a default super admin if none exists + const adminEmail = "admin@ayris.tech"; + + const existingAdmin = await prisma.user.findUnique({ + where: { email: adminEmail } + }); + + if (!existingAdmin) { + console.log(`Creating default admin: ${adminEmail}`); + await prisma.user.create({ + data: { + email: adminEmail, + name: "System Admin", + password: "admin123", // Should be changed immediately + role: "SUPER_ADMIN", + domains: ["*"], }, }); - } - - // 2. Migrate Mailbox Mappings - const mappingsRaw = process.env.MAIL_USER_MAPPINGS || "{}"; - try { - const mappings = JSON.parse(mappingsRaw); - for (const [email, userKey] of Object.entries(mappings)) { - const userIndex = parseInt((userKey as string).replace("USER_", "")); - const userEmail = process.env[`USER_${userIndex}_EMAIL`]; - - if (userEmail) { - const dbUser = await prisma.user.findUnique({ where: { email: userEmail } }); - if (dbUser) { - console.log(`Creating mapping: ${email} -> ${userEmail}`); - await prisma.mailboxMapping.upsert({ - where: { email }, - update: { userId: dbUser.id }, - create: { email, userId: dbUser.id }, - }); - } - } - } - } catch (e) { - console.error("Mapping migration failed:", e); + } else { + console.log("Admin user already exists."); } console.log("Seeding complete.");
{dict.users.username || "Kullanıcı"}{dict.users.role || "Rol"}{dict.users.domains || "İzin Verilen Domainler"}{dict.users.username}{dict.users.role}{dict.users.domains}Telegram ID
- {u.name[0]?.toUpperCase()} + {u.name ? u.name[0]?.toUpperCase() : "?"}
{u.name}
@@ -118,12 +188,12 @@ export default function UsersPage() {
- {u.role === "SUPER_ADMIN" ? `★ ${dict.users.superAdmin || "Süper Admin"}` : (dict.users.domainAdmin || "Domain Admin")} + {u.role === "SUPER_ADMIN" ? `★ ${dict.users.superAdmin}` : dict.users.domainAdmin} {u.domains.includes("*") ? ( - {dict.users.allDomains || "Tüm domainler"} + {dict.users.allDomains} ) : (
{u.domains.map((d) => ( @@ -132,6 +202,15 @@ export default function UsersPage() {
)}
+ {u.telegramId || "-"} + +
+ + +
+