Implement database migration, notification logs, and one-click Mailcow setup

This commit is contained in:
AyrisAI
2026-05-14 16:49:11 +03:00
parent f328296c64
commit b024e20027
18 changed files with 1067 additions and 166 deletions

24
app/api/logs/route.ts Normal file
View File

@@ -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 });
}
}

View File

@@ -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);
}

View File

@@ -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 });
}
}

43
app/api/mappings/route.ts Normal file
View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}