diff --git a/.gitignore b/.gitignore index 45254b6..00035ba 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ yarn-error.log* next-env.d.ts /app/generated/prisma + +/lib/generated/prisma diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 345d4d7..0376fa6 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -9,7 +9,8 @@ export async function GET() { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } - const users = getUsers().map(({ id, name, email, role, domains }) => ({ + const allUsers = await getUsers(); + const users = allUsers.map(({ id, name, email, role, domains }) => ({ id, name, email, diff --git a/app/api/webhooks/mail/route.ts b/app/api/webhooks/mail/route.ts index f69ac06..4d38e79 100644 --- a/app/api/webhooks/mail/route.ts +++ b/app/api/webhooks/mail/route.ts @@ -1,11 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; -import { getUsers } from "@/lib/users"; +import { prisma } from "@/lib/prisma"; /** * 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. + * Webhook endpoint for incoming mail notifications. + * Uses Prisma to look up user mappings in the database. */ export async function POST(req: NextRequest) { @@ -19,22 +19,15 @@ export async function POST(req: NextRequest) { console.log(`[Mail Webhook] Yeni mail geldi: ${sender} -> ${aliciMail}`); - // 1. Find which USER_X owns this mail address via JSON mapping - // Format: MAIL_USER_MAPPINGS='{"email1@domain.com":"USER_0", "email2@domain.com":"USER_1"}' - const mappingsRaw = process.env.MAIL_USER_MAPPINGS || "{}"; - let ownerUserKey: string | undefined = undefined; - - try { - const mappings = JSON.parse(mappingsRaw); - ownerUserKey = mappings[aliciMail]; - } catch (e) { - console.error("[Mail Webhook] MAIL_USER_MAPPINGS JSON ayrıştırma hatası:", e); - } + // 1. Find mapping in database + const mapping = await prisma.mailboxMapping.findUnique({ + where: { email: aliciMail }, + include: { user: true }, + }); - if (ownerUserKey) { - // 2. Get that USER's Telegram ID (e.g., USER_0_TELEGRAM_ID) - const tgIdKey = `${ownerUserKey}_TELEGRAM_ID`; - const targetChatId = process.env[tgIdKey]; + if (mapping?.user) { + const { user } = mapping; + const targetChatId = user.telegramId; if (targetChatId && process.env.TELEGRAM_BOT_TOKEN) { const message = `🔔 *Yeni Mail Geldi!*\n\n📧 *Alıcı:* ${aliciMail}\n👤 *Gönderen:* ${sender}\n📝 *Konu:* ${subject}`; @@ -55,7 +48,7 @@ export async function POST(req: NextRequest) { const errorText = await res.text(); console.error(`[Mail Webhook] Telegram API hatası: ${res.status} ${errorText}`); } else { - console.log(`[Webhook] Bildirim ${ownerUserKey} kullanıcısına (ID: ${targetChatId}) gönderildi.`); + console.log(`[Webhook] Bildirim ${user.email} kullanıcısına (ID: ${targetChatId}) gönderildi.`); } } } else { diff --git a/auth.ts b/auth.ts index 1d7e409..0dda299 100644 --- a/auth.ts +++ b/auth.ts @@ -16,7 +16,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ if (!email || !password) return null; - const user = authenticateUser(email, password); + const user = await authenticateUser(email, password); if (!user) return null; return { diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..5dcdc0b --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + // In Prisma 7, the connection URL should ideally come from prisma.config.ts + // or passed here if not using the new config system. + // For now, let's see if it picks up the URL automatically. + }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/lib/users.ts b/lib/users.ts index d86a017..4b1d181 100644 --- a/lib/users.ts +++ b/lib/users.ts @@ -16,54 +16,34 @@ * USER_1_DOMAINS="aveminakarabudak.com" */ +import { prisma } from "./prisma"; + export interface AppUser { - id: string; // "user_0", "user_1", ... - name: string; + id: string; + name: string | null; email: string; - 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 + password: string; + role: string; + domains: string[]; + telegramId?: string | null; } -/** Load all users defined in environment variables */ -export function getUsers(): AppUser[] { - const users: AppUser[] = []; - - let i = 0; - while (true) { - const name = process.env[`USER_${i}_NAME`]; - const email = process.env[`USER_${i}_EMAIL`]; - 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; - - users.push({ - id: `user_${i}`, - name, - email, - password, - role: role ?? "DOMAIN_ADMIN", - domains: domainsRaw === "*" ? ["*"] : domainsRaw.split(",").map((d) => d.trim()).filter(Boolean), - telegramId, - }); - - i++; - } - - return users; +/** Load all users from database */ +export async function getUsers(): Promise { + const users = await prisma.user.findMany(); + return users as AppUser[]; } -/** Find user by email and validate password */ -export function authenticateUser(email: string, password: string): AppUser | null { - const users = getUsers(); - const user = users.find((u) => u.email.toLowerCase() === email.toLowerCase()); +/** Find user by email and validate password via database */ +export async function authenticateUser(email: string, password: string): Promise { + const user = await prisma.user.findUnique({ + where: { email: email.toLowerCase() }, + }); + if (!user) return null; if (user.password !== password) return null; - return user; + + return user as AppUser; } /** Check if a user has access to a specific domain */ diff --git a/package-lock.json b/package-lock.json index 9ae91cc..5a46dae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@formatjs/intl-localematcher": "^0.8.7", + "@prisma/client": "^6.2.1", "@tanstack/react-query": "^5.100.10", "bcryptjs": "^3.0.3", "imapflow": "^1.3.3", @@ -34,6 +35,7 @@ "dotenv": "^17.4.2", "eslint": "^9", "eslint-config-next": "16.2.6", + "prisma": "^6.2.1", "tailwindcss": "^4", "tsx": "^4.21.0", "typescript": "^5" @@ -1757,6 +1759,74 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@prisma/client": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", + "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", + "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", + "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/fetch-engine": "6.2.1", + "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", + "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", + "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", + "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.2.1" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6516,6 +6586,26 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", + "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "6.2.1" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", diff --git a/package.json b/package.json index 81eb51e..d5cf8b3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@formatjs/intl-localematcher": "^0.8.7", + "@prisma/client": "^6.2.1", "@tanstack/react-query": "^5.100.10", "bcryptjs": "^3.0.3", "imapflow": "^1.3.3", @@ -39,6 +40,7 @@ "dotenv": "^17.4.2", "eslint": "^9", "eslint-config-next": "16.2.6", + "prisma": "^6.2.1", "tailwindcss": "^4", "tsx": "^4.21.0", "typescript": "^5" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d935ead --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,43 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String @unique + name String? + password String + role String @default("DOMAIN_ADMIN") // SUPER_ADMIN or DOMAIN_ADMIN + domains String[] @default([]) // ["*"] or list of domains + telegramId String? + mailboxMappings MailboxMapping[] + notificationConfigs NotificationConfig[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model MailboxMapping { + id String @id @default(cuid()) + email String @unique + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) +} + +model NotificationConfig { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + type String // e.g., "TELEGRAM", "WEBHOOK" + value String // e.g., chat_id or webhook url + active Boolean @default(true) + createdAt DateTime @default(now()) +} diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..58a8b34 --- /dev/null +++ b/scripts/seed.ts @@ -0,0 +1,66 @@ +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, + }, + }); + } + + // 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); + } + + console.log("Seeding complete."); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + });