chore: migrate to PostgreSQL with Prisma
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,3 +41,5 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
/app/generated/prisma
|
||||
|
||||
/lib/generated/prisma
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
2
auth.ts
2
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 {
|
||||
|
||||
15
lib/prisma.ts
Normal file
15
lib/prisma.ts
Normal 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;
|
||||
60
lib/users.ts
60
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<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
90
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
43
prisma/schema.prisma
Normal 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
66
scripts/seed.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user