chore: migrate to PostgreSQL with Prisma

This commit is contained in:
AyrisAI
2026-05-14 14:57:15 +03:00
parent 2642d254dc
commit f328296c64
10 changed files with 253 additions and 61 deletions

2
.gitignore vendored
View File

@@ -41,3 +41,5 @@ yarn-error.log*
next-env.d.ts
/app/generated/prisma
/lib/generated/prisma

View File

@@ -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,

View File

@@ -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;
// 1. Find mapping in database
const mapping = await prisma.mailboxMapping.findUnique({
where: { email: aliciMail },
include: { user: true },
});
try {
const mappings = JSON.parse(mappingsRaw);
ownerUserKey = mappings[aliciMail];
} catch (e) {
console.error("[Mail Webhook] MAIL_USER_MAPPINGS JSON ayrıştırma hatası:", e);
}
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 {

View File

@@ -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 {

15
lib/prisma.ts Normal file
View File

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

View File

@@ -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<AppUser[]> {
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<AppUser | null> {
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 */

90
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

43
prisma/schema.prisma Normal file
View File

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

66
scripts/seed.ts Normal file
View File

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