Compare commits
17 Commits
a0fcc90d31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99f9b51db8 | ||
|
|
47dced6f89 | ||
|
|
25cc2227c5 | ||
|
|
effd88adfe | ||
|
|
8aa8410d48 | ||
|
|
7f1a81977f | ||
|
|
1098668dc4 | ||
|
|
b0139b6cab | ||
|
|
ede38e80e4 | ||
|
|
b8648fb5f7 | ||
|
|
123d2c8d54 | ||
|
|
083bc5f546 | ||
|
|
add1876c11 | ||
|
|
c331162704 | ||
|
|
871dc8406b | ||
|
|
8a7923b1f2 | ||
|
|
b3ef5195c5 |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.npm
|
||||||
|
scratch
|
||||||
|
Dockerfile
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
0 verbose cli /opt/homebrew/Cellar/node@22/22.22.2/bin/node /opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/bin/npm-cli.js
|
|
||||||
1 info using npm@10.9.7
|
|
||||||
2 info using node@v22.22.2
|
|
||||||
3 silly config load:file:/opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/npmrc
|
|
||||||
4 silly config load:file:/Users/ayrisdev/Github/mailserver/.npmrc
|
|
||||||
5 silly config load:file:/opt/homebrew/etc/npmrc
|
|
||||||
6 verbose title npm exec prisma migrate dev --name add_notification_logs
|
|
||||||
7 verbose argv "exec" "--" "prisma" "migrate" "dev" "--name" "add_notification_logs"
|
|
||||||
8 verbose logfile logs-max:10 dir:/Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_30_23_576Z-
|
|
||||||
9 verbose logfile /Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_30_23_576Z-debug-0.log
|
|
||||||
10 silly logfile done cleaning log files
|
|
||||||
11 http fetch GET https://registry.npmjs.org/npm attempt 1 failed with ENOTFOUND
|
|
||||||
12 verbose cwd /Users/ayrisdev/Github/mailserver
|
|
||||||
13 verbose os Darwin 24.6.0
|
|
||||||
14 verbose node v22.22.2
|
|
||||||
15 verbose npm v10.9.7
|
|
||||||
16 verbose exit 1
|
|
||||||
17 verbose code 1
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
0 verbose cli /opt/homebrew/Cellar/node@22/22.22.2/bin/node /opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/bin/npm-cli.js
|
|
||||||
1 info using npm@10.9.7
|
|
||||||
2 info using node@v22.22.2
|
|
||||||
3 silly config load:file:/opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/npmrc
|
|
||||||
4 silly config load:file:/Users/ayrisdev/Github/mailserver/.npmrc
|
|
||||||
5 silly config load:file:/opt/homebrew/etc/npmrc
|
|
||||||
6 verbose title npm exec prisma migrate dev --name add_notification_logs
|
|
||||||
7 verbose argv "exec" "--" "prisma" "migrate" "dev" "--name" "add_notification_logs"
|
|
||||||
8 verbose logfile logs-max:10 dir:/Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_30_35_429Z-
|
|
||||||
9 verbose logfile /Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_30_35_429Z-debug-0.log
|
|
||||||
10 silly logfile done cleaning log files
|
|
||||||
11 verbose cwd /Users/ayrisdev/Github/mailserver
|
|
||||||
12 verbose os Darwin 24.6.0
|
|
||||||
13 verbose node v22.22.2
|
|
||||||
14 verbose npm v10.9.7
|
|
||||||
15 verbose exit 1
|
|
||||||
16 verbose code 1
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
0 verbose cli /opt/homebrew/Cellar/node@22/22.22.2/bin/node /opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/bin/npm-cli.js
|
|
||||||
1 info using npm@10.9.7
|
|
||||||
2 info using node@v22.22.2
|
|
||||||
3 silly config load:file:/opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/npmrc
|
|
||||||
4 silly config load:file:/Users/ayrisdev/Github/mailserver/.npmrc
|
|
||||||
5 silly config load:file:/opt/homebrew/etc/npmrc
|
|
||||||
6 verbose title npm exec prisma db push
|
|
||||||
7 verbose argv "exec" "--" "prisma" "db" "push"
|
|
||||||
8 verbose logfile logs-max:10 dir:/Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_31_06_094Z-
|
|
||||||
9 verbose logfile /Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_31_06_094Z-debug-0.log
|
|
||||||
10 silly logfile done cleaning log files
|
|
||||||
11 verbose cwd /Users/ayrisdev/Github/mailserver
|
|
||||||
12 verbose os Darwin 24.6.0
|
|
||||||
13 verbose node v22.22.2
|
|
||||||
14 verbose npm v10.9.7
|
|
||||||
15 verbose exit 1
|
|
||||||
16 verbose code 1
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
0 verbose cli /opt/homebrew/Cellar/node@22/22.22.2/bin/node /opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/bin/npm-cli.js
|
|
||||||
1 info using npm@10.9.7
|
|
||||||
2 info using node@v22.22.2
|
|
||||||
3 silly config load:file:/opt/homebrew/Cellar/node@22/22.22.2/lib/node_modules/npm/npmrc
|
|
||||||
4 silly config load:file:/Users/ayrisdev/Github/mailserver/.npmrc
|
|
||||||
5 silly config load:file:/opt/homebrew/etc/npmrc
|
|
||||||
6 verbose title npm exec tsx scratch/check_logs.ts
|
|
||||||
7 verbose argv "exec" "--" "tsx" "scratch/check_logs.ts"
|
|
||||||
8 verbose logfile logs-max:10 dir:/Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_56_06_958Z-
|
|
||||||
9 verbose logfile /Users/ayrisdev/Github/mailserver/.npm/_logs/2026-05-14T13_56_06_958Z-debug-0.log
|
|
||||||
10 silly logfile done cleaning log files
|
|
||||||
11 verbose cwd /Users/ayrisdev/Github/mailserver
|
|
||||||
12 verbose os Darwin 24.6.0
|
|
||||||
13 verbose node v22.22.2
|
|
||||||
14 verbose npm v10.9.7
|
|
||||||
15 verbose exit 1
|
|
||||||
16 verbose code 1
|
|
||||||
151
README.md
151
README.md
@@ -1,90 +1,67 @@
|
|||||||
# AyrisMail Central
|
# 🚀 AyrisMail Unified System
|
||||||
|
|
||||||
AyrisMail Central, Mailcow altyapısı üzerine inşa edilmiş, **veritabanı gerektirmeyen (Database-less)**, modern ve hızlı bir Webmail ve E-posta Yönetim İstemcisidir. Next.js App Router kullanılarak geliştirilmiştir ve kullanıcı e-posta güvenliğini en üst düzeyde tutmak için şifreleri veritabanında saklamaz; bunun yerine güvenli, şifrelenmiş çerezler (cookies) aracılığıyla doğrudan IMAP ve SMTP sunucularıyla iletişim kurar.
|
[TR] Mailcow altyapısı için geliştirilmiş, gerçek zamanlı bildirim ve e-posta yönetim platformu.
|
||||||
|
[EN] Real-time notification and email management platform developed for Mailcow infrastructure.
|
||||||
## 🚀 Özellikler
|
|
||||||
|
|
||||||
- **Modern ve Responsive Arayüz:** Gmail tarzı 3 sütunlu (Klasörler - Liste - Detay) "Glassmorphism" esintili karanlık tema tasarımı. Mobil uyumlu.
|
|
||||||
- **Database-less Mimari:** Kullanıcıların e-posta parolaları veritabanında saklanmaz. Her oturum doğrudan Mailcow IMAP/SMTP üzerinden doğrulanır.
|
|
||||||
- **Çoklu Kiracı (Multi-tenant) Yönetimi:** `.env` üzerinden tanımlanan roller (`SUPER_ADMIN`, `DOMAIN_ADMIN`) ile domain bazlı yetkilendirme.
|
|
||||||
- **Gelişmiş Mail Yönetimi:**
|
|
||||||
- Gelen kutusu, gönderilenler, çöp kutusu ve özel klasörler arası anında senkronizasyon.
|
|
||||||
- Mail okuma, yanıtlama, silme ve taşıma işlemleri.
|
|
||||||
- **Ek Dosya Desteği:**
|
|
||||||
- Gelen maillerdeki ekleri (PDF, Resim, Office dosyaları vb.) indirme ve önizleme.
|
|
||||||
- Yeni mail gönderirken Sürükle-Bırak (Drag & Drop) ile kolay dosya ekleme.
|
|
||||||
- **Güvenli HTML Render:** Gelen e-postalar, potansiyel XSS açıklarına karşı sanitize edilerek güvenli bir `iframe` içerisinde render edilir.
|
|
||||||
|
|
||||||
## 🛠️ Teknoloji Yığını
|
|
||||||
|
|
||||||
- **Frontend & Backend:** Next.js 16 (App Router, Turbopack)
|
|
||||||
- **Stil & Tasarım:** Özel CSS (Tailwind reset üzerine inşa edilmiş modern UI)
|
|
||||||
- **IMAP İstemcisi:** `imapflow` (Klasör okuma, mesaj çekme, taşıma, silme)
|
|
||||||
- **SMTP İstemcisi:** `nodemailer` (Mail gönderimi ve `Sent` klasörüne APPEND işlemi)
|
|
||||||
- **E-posta Ayrıştırma:** `mailparser` (Gelen raw RFC822 verilerini anlamlandırma)
|
|
||||||
- **Kimlik Doğrulama:** NextAuth.js (Custom Credentials Provider) & `iron-session` tarzı şifreli çerezler
|
|
||||||
|
|
||||||
## 📦 Kurulum ve Çalıştırma
|
|
||||||
|
|
||||||
### Gereksinimler
|
|
||||||
- Node.js 20+
|
|
||||||
- Çalışan bir Mailcow sunucusu
|
|
||||||
|
|
||||||
### 1. Depoyu Klonlayın
|
|
||||||
```bash
|
|
||||||
git clone https://git.ayris.tech/ayrisdev/webmailserver.git
|
|
||||||
cd webmailserver
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Bağımlılıkları Yükleyin
|
|
||||||
```bash
|
|
||||||
npm install --legacy-peer-deps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Çevresel Değişkenleri (.env) Ayarlayın
|
|
||||||
Proje dizininde bir `.env` dosyası oluşturun ve aşağıdaki değerleri kendi sunucunuza göre düzenleyin:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Mailcow API & IMAP/SMTP Sunucusu
|
|
||||||
MAILCOW_API_URL="https://mail.yourdomain.com"
|
|
||||||
MAILCOW_API_KEY="YOUR_MAILCOW_SUPER_ADMIN_API_KEY"
|
|
||||||
|
|
||||||
# NextAuth Güvenlik
|
|
||||||
NEXTAUTH_URL="http://localhost:3000"
|
|
||||||
AUTH_SECRET="super-secret-32-character-random-string"
|
|
||||||
|
|
||||||
# IMAP & SMTP Portları (Gerekirse)
|
|
||||||
IMAP_PORT=993
|
|
||||||
SMTP_PORT=587
|
|
||||||
|
|
||||||
# Multi-tenant Kullanıcı Tanımları (DB Kullanılmadığı İçin)
|
|
||||||
USER_0_NAME="Admin User"
|
|
||||||
USER_0_EMAIL="admin@yourdomain.com"
|
|
||||||
USER_0_PASSWORD="admin_password"
|
|
||||||
USER_0_ROLE="SUPER_ADMIN"
|
|
||||||
USER_0_DOMAINS="*"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Geliştirme Sunucusunu Başlatın
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
Uygulama `http://localhost:3000` adresinde çalışmaya başlayacaktır.
|
|
||||||
|
|
||||||
## 🐳 Docker ile Kurulum (Production)
|
|
||||||
|
|
||||||
Proje, standalone modda production için optimize edilmiş bir `Dockerfile` içerir. Coolify, CapRover veya standart Docker ortamlarında kolayca çalıştırılabilir.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t ayrismail-central .
|
|
||||||
docker run -p 3000:3000 --env-file .env ayrismail-central
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Güvenlik Yaklaşımı
|
|
||||||
|
|
||||||
- **Veri Saklanmaz:** E-postalar ve şifreler sunucuda barındırılmaz, her istekte Mailcow'dan anlık olarak çekilir.
|
|
||||||
- **TLS/SSL:** IMAP ve SMTP bağlantıları `secure: true` (veya STARTTLS) zorunluluğu ile çalışır.
|
|
||||||
- **XSS Koruması:** E-posta içerikleri `sanitizeHtml` fonksiyonu ile temizlenir ve izole `iframe` sandbox'ı içerisinde gösterilir.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
© 2026 AyrisTech - Tüm hakları saklıdır.
|
|
||||||
|
## 🌍 Language / Dil
|
||||||
|
- [Türkçe](#türkçe)
|
||||||
|
- [English](#english)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a name="türkçe"></a>
|
||||||
|
## 🇹🇷 Türkçe
|
||||||
|
|
||||||
|
AyrisMail, Mailcow sunucularını akıllı bir bildirim merkezine dönüştüren kapsamlı bir çözümdür. Sunucu seviyesindeki mailleri anlık olarak izler, analiz eder ve kullanıcı bazlı Telegram bildirimleri fırlatır.
|
||||||
|
|
||||||
|
### ✨ Öne Çıkan Özellikler
|
||||||
|
- **Anlık Bildirimler:** Worker motoru sayesinde mail sunucuya düştüğü anda Telegram bildirimi.
|
||||||
|
- **Akıllı Kod Çözücü:** RFC 2047 (MIME) desteği ile Türkçe karakterli ve kodlanmış başlıkları otomatik çözer.
|
||||||
|
- **Gelişmiş Panel:** Mailbox-Kullanıcı eşleşmelerini ve bildirim loglarını yönetebileceğiniz modern arayüz.
|
||||||
|
- **Güvenli Mimari:** Docker tabanlı, PostgreSQL veritabanlı ve Webhook korumalı yapı.
|
||||||
|
- **Çoklu Kullanıcı:** Her kullanıcı sadece kendine atanan mail kutularının bildirimlerini alır.
|
||||||
|
|
||||||
|
### 🛠️ Teknoloji Yığını
|
||||||
|
- **Core:** Next.js 16 (App Router), TypeScript
|
||||||
|
- **Database:** PostgreSQL & Prisma ORM
|
||||||
|
- **Worker:** Node.js, Chokidar (File Watcher), Doveadm
|
||||||
|
- **Deployment:** Docker & Docker Compose
|
||||||
|
|
||||||
|
### 🚀 Hızlı Kurulum
|
||||||
|
1. `.env.example` dosyasını `.env` olarak kopyalayın ve bilgilerinizi girin.
|
||||||
|
2. Sistemi başlatın:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.unified.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a name="english"></a>
|
||||||
|
## 🇺🇸 English
|
||||||
|
|
||||||
|
AyrisMail is a comprehensive solution that transforms Mailcow servers into an intelligent notification hub. It monitors server-level emails in real-time, analyzes them, and triggers user-based Telegram alerts.
|
||||||
|
|
||||||
|
### ✨ Key Features
|
||||||
|
- **Instant Notifications:** Telegram alerts triggered the moment an email hits the server via our custom Worker engine.
|
||||||
|
- **Smart MIME Decoder:** Automatically decodes RFC 2047 encoded subjects (UTF-8, Base64, etc.) for readable notifications.
|
||||||
|
- **Advanced Dashboard:** Modern UI to manage Mailbox-to-User mappings and track notification logs.
|
||||||
|
- **Secure Architecture:** Docker-based setup with PostgreSQL and protected Webhook signals.
|
||||||
|
- **Multi-Tenant:** Each user receives notifications only for their assigned mailboxes.
|
||||||
|
|
||||||
|
### 🛠️ Tech Stack
|
||||||
|
- **Core:** Next.js 16 (App Router), TypeScript
|
||||||
|
- **Database:** PostgreSQL & Prisma ORM
|
||||||
|
- **Worker:** Node.js, Chokidar (File Watcher), Doveadm
|
||||||
|
- **Deployment:** Docker & Docker Compose
|
||||||
|
|
||||||
|
### 🚀 Quick Start
|
||||||
|
1. Copy `.env.example` to `.env` and fill in your credentials.
|
||||||
|
2. Start the system:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.unified.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
© 2026 AyrisTech - Advanced Agentic Coding Systems
|
||||||
|
|||||||
60
README_INSTALL.md
Normal file
60
README_INSTALL.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# 🚀 AyrisMail - Hepsi Bir Arada Kurulum Rehberi
|
||||||
|
|
||||||
|
Bu proje, Mailcow sunucunuzdaki mailleri anlık olarak izleyen ve Telegram üzerinden bildirim gönderen akıllı bir bildirim sistemidir.
|
||||||
|
|
||||||
|
## 📦 Sistem Bileşenleri
|
||||||
|
1. **Next.js Web Paneli:** Mailbox eşleşmelerini ve bildirim loglarını yönettiğiniz arayüz.
|
||||||
|
2. **PostgreSQL:** Tüm kullanıcı ve log verilerinin saklandığı veritabanı.
|
||||||
|
3. **Ayris Worker:** Sunucu seviyesinde mailleri anlık çözen ve Next.js'e sinyal atan motor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Kurulum Adımları
|
||||||
|
|
||||||
|
### 1. Dosya Yapısını Hazırlayın
|
||||||
|
Projeyi şu klasör yapısında olacak şekilde sunucunuza yerleştirin:
|
||||||
|
```text
|
||||||
|
/ayris-project
|
||||||
|
├── mailserver/ # Bu repo (Next.js)
|
||||||
|
├── ayristech-worker/ # Worker repo
|
||||||
|
└── docker-compose.unified.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Çevre Değişkenlerini Ayarlayın
|
||||||
|
`mailserver` klasörü içindeki `.env` dosyasını şu değişkenlerle doldurun:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Veritabanı (Docker içinde otomatik bağlanır)
|
||||||
|
DATABASE_URL="postgresql://postgres:ayris_pass_123@db:5432/ayrismail"
|
||||||
|
|
||||||
|
# Mailcow Bilgileri
|
||||||
|
MAILCOW_API_URL="https://mail.alanadiniz.com"
|
||||||
|
MAILCOW_API_KEY="your-api-key"
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
TELEGRAM_BOT_TOKEN="your-bot-token"
|
||||||
|
|
||||||
|
# Güvenlik
|
||||||
|
WEBHOOK_SIGNAL_SECRET="guclu-bir-sifre"
|
||||||
|
AUTH_SECRET="auth-secret-key"
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sistemi Başlatın
|
||||||
|
Terminalden ana dizine girin ve şu komutu çalıştırın:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.unified.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Güvenlik Notları
|
||||||
|
- `/var/run/docker.sock` dosyasının bağlanması, Worker'ın Dovecot konteynerine komut göndermesini sağlar.
|
||||||
|
- `WEBHOOK_SIGNAL_SECRET` bilgisinin hem `.env` hem de Worker konfigürasyonunda aynı olduğundan emin olun.
|
||||||
|
|
||||||
|
## 📝 Kullanım
|
||||||
|
1. `http://sunucu-ip:3000` adresinden panele giriş yapın.
|
||||||
|
2. Kullanıcı oluşturun ve Telegram ID'sini girin.
|
||||||
|
3. Mailbox Mapping kısmından hangi mailin kime gideceğini tanımlayın.
|
||||||
|
4. Worker mailleri yakaladıkça bildirimler anında Telegram'a düşecektir.
|
||||||
63
app/[lang]/dashboard/DashboardLayoutClient.tsx
Normal file
63
app/[lang]/dashboard/DashboardLayoutClient.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Providers from "@/components/Providers";
|
||||||
|
import Sidebar from "@/components/Sidebar";
|
||||||
|
import { DictionaryProvider } from "@/components/DictionaryContext";
|
||||||
|
|
||||||
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
dict,
|
||||||
|
lang,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
dict: any;
|
||||||
|
lang: string;
|
||||||
|
}) {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Providers>
|
||||||
|
<DictionaryProvider dictionary={dict}>
|
||||||
|
<div className={`app-layout ${isSidebarOpen ? "sidebar-open" : ""}`}>
|
||||||
|
{/* Mobile Overlay */}
|
||||||
|
<div
|
||||||
|
className="sidebar-overlay"
|
||||||
|
onClick={() => setIsSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Sidebar
|
||||||
|
dict={dict}
|
||||||
|
lang={lang}
|
||||||
|
onClose={() => setIsSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="main-content">
|
||||||
|
{/* Mobile Header */}
|
||||||
|
<header className="mobile-header">
|
||||||
|
<button
|
||||||
|
className="mobile-menu-btn"
|
||||||
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</button>
|
||||||
|
<div className="mobile-logo">AyrisMail</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DictionaryProvider>
|
||||||
|
</Providers>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="4" x2="20" y1="12" y2="12" />
|
||||||
|
<line x1="4" x2="20" y1="6" y2="6" />
|
||||||
|
<line x1="4" x2="20" y1="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import Providers from "@/components/Providers";
|
|
||||||
import Sidebar from "@/components/Sidebar";
|
|
||||||
import { getDictionary, Locale } from "@/app/dictionaries";
|
import { getDictionary, Locale } from "@/app/dictionaries";
|
||||||
import { DictionaryProvider } from "@/components/DictionaryContext";
|
import DashboardLayoutClient from "./DashboardLayoutClient";
|
||||||
|
|
||||||
export default async function DashboardLayout(
|
export default async function DashboardLayout(
|
||||||
props: {
|
props: {
|
||||||
@@ -12,10 +10,7 @@ export default async function DashboardLayout(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
const { children } = props;
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session) redirect(`/${params.lang}/login`);
|
if (!session) redirect(`/${params.lang}/login`);
|
||||||
@@ -23,13 +18,8 @@ export default async function DashboardLayout(
|
|||||||
const dict = await getDictionary(params.lang as Locale);
|
const dict = await getDictionary(params.lang as Locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Providers>
|
<DashboardLayoutClient dict={dict} lang={params.lang}>
|
||||||
<DictionaryProvider dictionary={dict}>
|
{children}
|
||||||
<div className="app-layout">
|
</DashboardLayoutClient>
|
||||||
<Sidebar dict={dict} lang={params.lang} />
|
|
||||||
<div className="main-content">{children}</div>
|
|
||||||
</div>
|
|
||||||
</DictionaryProvider>
|
|
||||||
</Providers>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
196
app/[lang]/dashboard/settings/page.tsx
Normal file
196
app/[lang]/dashboard/settings/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useDictionary } from "@/components/DictionaryContext";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [profile, setProfile] = useState<any>(null);
|
||||||
|
const [waStatus, setWaStatus] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [fetchingQr, setFetchingQr] = useState(false);
|
||||||
|
const dict = useDictionary();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProfile();
|
||||||
|
fetchWaStatus();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchWaStatus();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/users/profile");
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.error) setError(data.error);
|
||||||
|
else if (!data) setError("Kullanıcı profili bulunamadı.");
|
||||||
|
else setProfile(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchWaStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/whatsapp/status");
|
||||||
|
const data = await res.json();
|
||||||
|
setWaStatus(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnectWa = async () => {
|
||||||
|
setFetchingQr(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/whatsapp/qr");
|
||||||
|
const data = await res.json();
|
||||||
|
setWaStatus(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setFetchingQr(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/users/profile", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(profile)
|
||||||
|
});
|
||||||
|
if (res.ok) alert("Ayarlar kaydedildi!");
|
||||||
|
else {
|
||||||
|
const data = await res.json();
|
||||||
|
alert("Hata: " + (data?.error || "Bilinmeyen bir hata oluştu."));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
alert("Hata: " + e.message);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div className="page-body"><span className="spinner" /></div>;
|
||||||
|
if (error) return <div className="page-body"><div className="card" style={{ color: "var(--error)" }}>Hata: {error}</div></div>;
|
||||||
|
if (!profile) return <div className="page-body">Profil yüklenemedi.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="page-title">Bildirim Ayarları</h1>
|
||||||
|
<p className="page-subtitle">Telegram ve WhatsApp bildirimlerinizi buradan yönetin.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-body" style={{ maxWidth: 800 }}>
|
||||||
|
<form onSubmit={handleSave} className="form-group">
|
||||||
|
<div className="card">
|
||||||
|
<h3 style={{ marginBottom: 20, display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
|
<TelegramIcon /> Telegram Bildirimleri
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 20 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="tg-enabled"
|
||||||
|
checked={profile.telegramEnabled}
|
||||||
|
onChange={e => setProfile({...profile, telegramEnabled: e.target.checked})}
|
||||||
|
/>
|
||||||
|
<label htmlFor="tg-enabled">Telegram bildirimlerini aktif et</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Telegram ID</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={profile.telegramId || ""}
|
||||||
|
onChange={e => setProfile({...profile, telegramId: e.target.value})}
|
||||||
|
placeholder="Örn: 5009005027"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card" style={{ marginTop: 24 }}>
|
||||||
|
<h3 style={{ marginBottom: 20, display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
|
<WhatsAppIcon /> WhatsApp Bildirimleri
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 20 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="wa-enabled"
|
||||||
|
checked={profile.whatsappEnabled}
|
||||||
|
onChange={e => setProfile({...profile, whatsappEnabled: e.target.checked})}
|
||||||
|
/>
|
||||||
|
<label htmlFor="wa-enabled">WhatsApp bildirimlerini aktif et</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
|
||||||
|
<div>
|
||||||
|
<label className="label">Telefon Numarası</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={profile.whatsappNumber || ""}
|
||||||
|
onChange={e => setProfile({...profile, whatsappNumber: e.target.value})}
|
||||||
|
placeholder="90554XXXXXXX"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ borderLeft: "1px solid var(--border)", paddingLeft: 24 }}>
|
||||||
|
<label className="label">Bağlantı Durumu</label>
|
||||||
|
{waStatus?.status === 'connected' ? (
|
||||||
|
<div style={{ color: "#10b981", fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#10b981" }} />
|
||||||
|
Bağlı ✅
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ color: "#ef4444", fontWeight: 600, display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||||
|
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#ef4444" }} />
|
||||||
|
Bağlı Değil
|
||||||
|
</div>
|
||||||
|
{waStatus?.qr ? (
|
||||||
|
<div style={{ background: "#fff", padding: 10, borderRadius: 8, width: "fit-content" }}>
|
||||||
|
<img src={waStatus.qr} alt="QR Code" style={{ width: 150, height: 150 }} />
|
||||||
|
<p style={{ fontSize: 11, color: "#000", textAlign: "center", marginTop: 5 }}>WhatsApp'tan okutun</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
onClick={handleConnectWa}
|
||||||
|
disabled={fetchingQr}
|
||||||
|
>
|
||||||
|
{fetchingQr ? "QR Oluşturuluyor..." : "Bağlantı Kur (QR)"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 24, display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||||
|
{saving ? <span className="spinner" /> : "Değişiklikleri Kaydet"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TelegramIcon() { return <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>; }
|
||||||
|
function WhatsAppIcon() { return <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z"/></svg>; }
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { useDictionary } from "@/components/DictionaryContext";
|
import { useDictionary } from "@/components/DictionaryContext";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -19,6 +20,7 @@ export default function UsersPage() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -242,6 +244,7 @@ export default function UsersPage() {
|
|||||||
<input className="input" type="password" value={formData.password} onChange={e => setFormData({...formData, password: e.target.value})} required={!editingUser} />
|
<input className="input" type="password" value={formData.password} onChange={e => setFormData({...formData, password: e.target.value})} required={!editingUser} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
|
||||||
|
{session?.user?.role === "SUPER_ADMIN" ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Rol</label>
|
<label className="label">Rol</label>
|
||||||
<select className="input" value={formData.role} onChange={e => setFormData({...formData, role: e.target.value})}>
|
<select className="input" value={formData.role} onChange={e => setFormData({...formData, role: e.target.value})}>
|
||||||
@@ -249,15 +252,28 @@ export default function UsersPage() {
|
|||||||
<option value="DOMAIN_ADMIN">Domain Admin</option>
|
<option value="DOMAIN_ADMIN">Domain Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<label className="label">Rol</label>
|
||||||
|
<input className="input" value="Domain Admin" disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Telegram ID</label>
|
<label className="label">Telegram ID</label>
|
||||||
<input className="input" placeholder="Örn: 5009005027" value={formData.telegramId} onChange={e => setFormData({...formData, telegramId: e.target.value})} />
|
<input className="input" placeholder="Örn: 5009005027" value={formData.telegramId} onChange={e => setFormData({...formData, telegramId: e.target.value})} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{session?.user?.role === "SUPER_ADMIN" ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="label">İzinli Domainler (Virgülle ayırın, tümü için *)</label>
|
<label className="label">İzinli Domainler (Virgülle ayırın, tümü için *)</label>
|
||||||
<input className="input" placeholder="domain1.com, domain2.com" value={formData.domains} onChange={e => setFormData({...formData, domains: e.target.value})} />
|
<input className="input" placeholder="domain1.com, domain2.com" value={formData.domains} onChange={e => setFormData({...formData, domains: e.target.value})} />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<label className="label">İzinli Domainler</label>
|
||||||
|
<input className="input" value={session?.user?.domains?.join(", ")} disabled />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={{ display: "flex", gap: 10, marginTop: 10 }}>
|
<div style={{ display: "flex", gap: 10, marginTop: 10 }}>
|
||||||
<button type="button" className="btn btn-ghost" style={{ flex: 1 }} onClick={() => setIsModalOpen(false)}>İptal</button>
|
<button type="button" className="btn btn-ghost" style={{ flex: 1 }} onClick={() => setIsModalOpen(false)}>İptal</button>
|
||||||
<button type="submit" className="btn btn-primary" style={{ flex: 1 }} disabled={saving}>
|
<button type="submit" className="btn btn-primary" style={{ flex: 1 }} disabled={saving}>
|
||||||
|
|||||||
@@ -7,22 +7,44 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
if (!session || session.user.role !== "SUPER_ADMIN") {
|
if (!session) {
|
||||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userRole = session.user.role;
|
||||||
|
const adminDomains = session.user.domains || [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Mevcut kullanıcıyı kontrol et
|
||||||
|
const existingUser = await prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!existingUser) return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
// Güvenlik Kontrolü: Domain admin sadece kendi domainindeki kullanıcıyı güncelleyebilir
|
||||||
|
if (userRole !== "SUPER_ADMIN") {
|
||||||
|
const hasAccess = existingUser.domains.some(d => adminDomains.includes(d));
|
||||||
|
if (!hasAccess) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { name, email, password, role, domains, telegramId } = body;
|
const { name, email, password, role, domains, telegramId } = body;
|
||||||
|
|
||||||
|
let finalDomains = domains;
|
||||||
|
let finalRole = role;
|
||||||
|
|
||||||
|
// Güvenlik: Domain admin yetki yükseltemez veya domain değiştiremez
|
||||||
|
if (userRole !== "SUPER_ADMIN") {
|
||||||
|
finalDomains = adminDomains; // Kendi domainlerine kilitler
|
||||||
|
finalRole = "DOMAIN_ADMIN";
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.update({
|
const user = await prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
email: email?.toLowerCase(),
|
email: email?.toLowerCase(),
|
||||||
password,
|
password,
|
||||||
role,
|
role: finalRole,
|
||||||
domains,
|
domains: finalDomains,
|
||||||
telegramId,
|
telegramId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -38,11 +60,23 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
if (!session || session.user.role !== "SUPER_ADMIN") {
|
if (!session) {
|
||||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userRole = session.user.role;
|
||||||
|
const adminDomains = session.user.domains || [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const existingUser = await prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!existingUser) return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
|
// Güvenlik Kontrolü
|
||||||
|
if (userRole !== "SUPER_ADMIN") {
|
||||||
|
const hasAccess = existingUser.domains.some(d => adminDomains.includes(d));
|
||||||
|
if (!hasAccess) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.user.delete({
|
await prisma.user.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|||||||
38
app/api/users/profile/route.ts
Normal file
38
app/api/users/profile/route.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const { telegramId, telegramEnabled, whatsappNumber, whatsappEnabled } = body;
|
||||||
|
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
data: {
|
||||||
|
telegramId,
|
||||||
|
telegramEnabled,
|
||||||
|
whatsappNumber,
|
||||||
|
whatsappEnabled
|
||||||
|
} as any
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,30 @@ import { prisma } from "@/lib/prisma";
|
|||||||
// GET /api/users — list all users
|
// GET /api/users — list all users
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session || session.user.role !== "SUPER_ADMIN") {
|
if (!session) {
|
||||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const userRole = session.user.role;
|
||||||
|
const userDomains = session.user.domains || [];
|
||||||
|
|
||||||
|
let users;
|
||||||
|
if (userRole === "SUPER_ADMIN") {
|
||||||
|
// Super admin her şeyi görür
|
||||||
|
users = await prisma.user.findMany({
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Domain admin sadece kendi domainlerine dokunan kullanıcıları görür
|
||||||
|
users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
domains: {
|
||||||
|
hasSome: userDomains
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(users);
|
return NextResponse.json(users);
|
||||||
}
|
}
|
||||||
@@ -19,21 +36,34 @@ export async function GET() {
|
|||||||
// POST /api/users — create a new user
|
// POST /api/users — create a new user
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session || session.user.role !== "SUPER_ADMIN") {
|
if (!session) {
|
||||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userRole = session.user.role;
|
||||||
|
const adminDomains = session.user.domains || [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { name, email, password, role, domains, telegramId } = body;
|
const { name, email, password, role, domains, telegramId } = body;
|
||||||
|
|
||||||
|
let finalDomains = domains || [];
|
||||||
|
let finalRole = role || "DOMAIN_ADMIN";
|
||||||
|
|
||||||
|
// Güvenlik: Domain admin yetkisini aşamaz
|
||||||
|
if (userRole !== "SUPER_ADMIN") {
|
||||||
|
// Eğer domain admin ise, yeni kullanıcıya sadece kendi domainlerini verebilir
|
||||||
|
finalDomains = adminDomains;
|
||||||
|
finalRole = "DOMAIN_ADMIN"; // Başka bir super admin oluşturamaz
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password,
|
password,
|
||||||
role: role || "DOMAIN_ADMIN",
|
role: finalRole,
|
||||||
domains: domains || [],
|
domains: finalDomains,
|
||||||
telegramId,
|
telegramId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { getLatestEmail } from '@/lib/mail';
|
|
||||||
import { sendTelegramNotification } from '@/lib/notifications';
|
import { sendTelegramNotification } from '@/lib/notifications';
|
||||||
|
import { sendWA } from '@/lib/whatsapp';
|
||||||
|
|
||||||
// Bu kısım normalde .env içinde olmalı
|
// Bu kısım normalde .env içinde olmalı
|
||||||
const WEBHOOK_SECRET = 'besiktasK1903*';
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SIGNAL_SECRET || 'besiktasK1903*';
|
||||||
const IMAP_PASSWORD = process.env.MAILCOW_MASTER_PASSWORD || ''; // Dovecot Master Password tavsiye edilir
|
|
||||||
|
// RFC 2047 Decode Fonksiyonu
|
||||||
|
function decodeMimeText(text: string) {
|
||||||
|
if (!text) return text;
|
||||||
|
|
||||||
|
// Eğer metin =? ile başlıyorsa decode etmeye çalış
|
||||||
|
return text.replace(/=\?([^?]+)\?([QB])\?([^?]+)\?=/gi, (match, charset, encoding, data) => {
|
||||||
|
try {
|
||||||
|
if (encoding.toUpperCase() === 'Q') {
|
||||||
|
// Quoted-Printable decode
|
||||||
|
return data
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/=([0-9A-F]{2})/gi, (_: any, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
||||||
|
} else if (encoding.toUpperCase() === 'B') {
|
||||||
|
// Base64 decode
|
||||||
|
return Buffer.from(data, 'base64').toString(charset.toLowerCase() === 'utf-8' ? 'utf8' : 'binary');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const secret = request.headers.get('x-ayristech-secret');
|
const secret = request.headers.get('x-ayristech-secret');
|
||||||
@@ -15,9 +37,10 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { to, event, subject: incomingSubject, body: incomingBody, from: incomingFrom } = body;
|
const { to, event, subject: incomingSubject, snippet: incomingSnippet, from: incomingFrom } = body;
|
||||||
|
|
||||||
console.log(`📩 Webhook Sinyali Alındı! Alıcı: ${to}`);
|
const subject = decodeMimeText(incomingSubject || "");
|
||||||
|
console.log(`📩 Webhook Sinyali Alındı! Alıcı: ${to} | Konu: ${subject}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Mailbox Mapping kontrolü
|
// 1. Mailbox Mapping kontrolü
|
||||||
@@ -31,70 +54,58 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ success: true, message: 'No mapping found' });
|
return NextResponse.json({ success: true, message: 'No mapping found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Mail İçeriğini Belirle (Ya gelen body'den ya da IMAP'ten)
|
// 2. Mail İçeriğini Belirle
|
||||||
let mailData = null;
|
if (!incomingSubject && !incomingSnippet) {
|
||||||
|
console.log(`[Signal] İçerik eksik, işlem durduruldu: ${to}`);
|
||||||
|
return NextResponse.json({ success: true, message: 'No content provided' });
|
||||||
|
}
|
||||||
|
|
||||||
if (incomingSubject && incomingBody) {
|
const mailData = {
|
||||||
console.log("[Signal] İçerik worker'dan hazır geldi.");
|
subject: subject || "(Konu Yok)",
|
||||||
mailData = {
|
text: incomingSnippet || "",
|
||||||
subject: incomingSubject,
|
|
||||||
text: incomingBody,
|
|
||||||
from: incomingFrom || "Bilinmiyor"
|
from: incomingFrom || "Bilinmiyor"
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
console.log("[Signal] İçerik eksik, IMAP'e gidiliyor...");
|
|
||||||
mailData = await getLatestEmail(to, IMAP_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mailData) {
|
// 3. Bildirim Gönder (Telegram)
|
||||||
console.error(`[Signal] Mail içeriği çekilemedi: ${to}`);
|
let tgStatus = 'SKIPPED';
|
||||||
return NextResponse.json({ success: false, error: 'Could not fetch mail' }, { status: 500 });
|
const user = mapping.user as any;
|
||||||
}
|
if (user.telegramEnabled && user.telegramId) {
|
||||||
|
const tgResult = await sendTelegramNotification(
|
||||||
console.log(`[Signal] Mail İşleniyor: "${mailData.subject}"`);
|
|
||||||
|
|
||||||
// 3. İçerik Analizi (BMW, Penti vb.)
|
|
||||||
let processed = false;
|
|
||||||
let extraInfo = "";
|
|
||||||
const content = (mailData.subject + " " + mailData.text).toLowerCase();
|
|
||||||
|
|
||||||
if (content.includes("bmw") || content.includes("tamir")) {
|
|
||||||
console.log("🚗 [Signal] BMW/Tamir içerikli mail tespit edildi!");
|
|
||||||
extraInfo = "🚗 BMW/Tamir İlgili İçerik";
|
|
||||||
processed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.includes("penti") || content.includes("sipariş")) {
|
|
||||||
console.log("🛍️ [Signal] Penti/Sipariş içerikli mail tespit edildi!");
|
|
||||||
extraInfo = "🛍️ Penti/Sipariş İlgili İçerik";
|
|
||||||
processed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Bildirim Gönder (Telegram)
|
|
||||||
const notificationResult = await sendTelegramNotification(
|
|
||||||
mapping.userId,
|
mapping.userId,
|
||||||
to,
|
to,
|
||||||
mailData.from,
|
mailData.from,
|
||||||
mailData.subject,
|
mailData.subject,
|
||||||
extraInfo
|
""
|
||||||
);
|
);
|
||||||
|
tgStatus = tgResult.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Bildirim Gönder (WhatsApp)
|
||||||
|
let waStatus = 'SKIPPED';
|
||||||
|
if (user.whatsappEnabled && (user.whatsappNumber || process.env.DEFAULT_WHATSAPP_NUMBER)) {
|
||||||
|
const waNumber = user.whatsappNumber || process.env.DEFAULT_WHATSAPP_NUMBER;
|
||||||
|
const waMessage = `📩 *Yeni E-posta*\n\n*Gönderen:* ${mailData.from}\n*Konu:* ${mailData.subject}\n*Alıcı:* ${to}\n\n_AyrisMail Central_`;
|
||||||
|
|
||||||
|
const waResult = await sendWA(waNumber, waMessage, mapping.userId);
|
||||||
|
waStatus = waResult.success ? 'SENT' : 'FAILED';
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Bildirim Logu
|
// 5. Bildirim Logu
|
||||||
await prisma.notificationLog.create({
|
await (prisma as any).notificationLog.create({
|
||||||
data: {
|
data: {
|
||||||
mailbox: to,
|
mailbox: to,
|
||||||
sender: mailData.from,
|
sender: mailData.from,
|
||||||
subject: mailData.subject,
|
subject: mailData.subject,
|
||||||
status: notificationResult.status,
|
status: tgStatus === 'SENT' || waStatus === 'SENT' ? 'SENT' : 'FAILED',
|
||||||
userId: mapping.userId,
|
userId: mapping.userId,
|
||||||
error: notificationResult.error || (processed ? null : "Anahtar kelime eşleşmedi")
|
error: tgStatus === 'FAILED' ? 'TG Failed' : (waStatus === 'FAILED' ? 'WA Failed' : null)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
processed,
|
tgStatus,
|
||||||
notification: notificationResult.status,
|
waStatus,
|
||||||
subject: mailData.subject
|
subject: mailData.subject
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
19
app/api/whatsapp/qr/route.ts
Normal file
19
app/api/whatsapp/qr/route.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
const workerUrl = process.env.WHATSAPP_WORKER_URL;
|
||||||
|
const secret = process.env.WHATSAPP_SECRET;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${workerUrl}/get-qr?userId=${userId}&secret=${secret}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ status: 'error', error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/api/whatsapp/status/route.ts
Normal file
19
app/api/whatsapp/status/route.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
const workerUrl = process.env.WHATSAPP_WORKER_URL;
|
||||||
|
const secret = process.env.WHATSAPP_SECRET;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${workerUrl}/status?userId=${userId}&secret=${secret}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({ status: 'error', error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1085,6 +1085,46 @@ tr:hover td {
|
|||||||
background: var(--danger-dim);
|
background: var(--danger-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Mobile Header ── */
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 90;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.mail-layout { grid-template-columns: 60px 280px 1fr; }
|
.mail-layout { grid-template-columns: 60px 280px 1fr; }
|
||||||
@@ -1097,12 +1137,42 @@ tr:hover td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar { display: none; }
|
.mobile-header { display: flex; }
|
||||||
.mail-layout { grid-template-columns: 1fr; }
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: -240px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 10px 0 30px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-open .sidebar {
|
||||||
|
transform: translateX(240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-open .sidebar-overlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-layout { grid-template-columns: 1fr; height: auto; overflow: visible; }
|
||||||
.mail-sidebar { display: none; }
|
.mail-sidebar { display: none; }
|
||||||
.mail-detail { display: none; }
|
.mail-detail { display: none; }
|
||||||
|
|
||||||
|
/* Show active mail view if selected */
|
||||||
|
.mail-view-active .mail-list { display: none; }
|
||||||
|
.mail-view-active .mail-detail { display: block; }
|
||||||
|
|
||||||
.page-body { padding: 16px; }
|
.page-body { padding: 16px; }
|
||||||
.page-header { padding: 16px; }
|
.page-header {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.page-header .btn { width: 100%; justify-content: center; }
|
||||||
|
|
||||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||||
}
|
}
|
||||||
/* ── Language Switcher ── */
|
/* ── Language Switcher ── */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import LanguageSwitcher from "./LanguageSwitcher";
|
import LanguageSwitcher from "./LanguageSwitcher";
|
||||||
|
|
||||||
export default function Sidebar({ dict, lang }: { dict: any; lang: string }) {
|
export default function Sidebar({ dict, lang, onClose }: { dict: any; lang: string; onClose?: () => void }) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const role = session?.user?.role ?? "";
|
const role = session?.user?.role ?? "";
|
||||||
@@ -18,14 +18,15 @@ export default function Sidebar({ dict, lang }: { dict: any; lang: string }) {
|
|||||||
items: [
|
items: [
|
||||||
{ href: `/${lang}/dashboard`, label: dict.dashboard?.title || "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
{ href: `/${lang}/dashboard`, label: dict.dashboard?.title || "Dashboard", icon: HomeIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
||||||
{ href: `/${lang}/dashboard/mail`, label: dict.sidebar?.mailClient || "Mail Client", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
{ href: `/${lang}/dashboard/mail`, label: dict.sidebar?.mailClient || "Mail Client", icon: InboxIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
||||||
|
{ href: `/${lang}/dashboard/settings`, label: "Ayarlar", icon: SettingsIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: dict.sidebar?.management || "YÖNETİM",
|
section: dict.sidebar?.management || "YÖNETİM",
|
||||||
items: [
|
items: [
|
||||||
{ href: `/${lang}/dashboard/domains`, label: dict.domains?.title || "Domainler", icon: GlobeIcon, roles: ["SUPER_ADMIN"] },
|
{ 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/users`, label: dict.sidebar?.users || "Kullanıcılar", icon: UsersIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
||||||
{ href: `/${lang}/dashboard/mappings`, label: dict.sidebar?.mappings || "Eşleştirmeler", icon: LinkIcon, roles: ["SUPER_ADMIN"] },
|
{ href: `/${lang}/dashboard/mappings`, label: dict.sidebar?.mappings || "Eşleştirmeler", icon: LinkIcon, roles: ["SUPER_ADMIN", "DOMAIN_ADMIN"] },
|
||||||
{ href: `/${lang}/dashboard/mailboxes`, label: dict.sidebar?.mailboxes || "Mail Hesapları", icon: MailIcon, roles: ["SUPER_ADMIN", "DOMAIN_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"] },
|
{ href: `/${lang}/dashboard/logs`, label: dict.sidebar?.logs || "Loglar", icon: ListIcon, roles: ["SUPER_ADMIN"] },
|
||||||
],
|
],
|
||||||
@@ -60,6 +61,7 @@ export default function Sidebar({ dict, lang }: { dict: any; lang: string }) {
|
|||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`nav-item ${pathname === item.href ? "active" : ""}`}
|
className={`nav-item ${pathname === item.href ? "active" : ""}`}
|
||||||
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<item.icon />
|
<item.icon />
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -176,3 +178,12 @@ function ListIcon() {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
53
docker-compose.unified.yml
Normal file
53
docker-compose.unified.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 1. Veritabanı
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: ayrismail-db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ayris_pass_123}
|
||||||
|
POSTGRES_DB: ayrismail
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# 2. Next.js Uygulaması (Panel)
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ./mailserver
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ayrismail-app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD:-ayris_pass_123}@db:5432/ayrismail"
|
||||||
|
MAILCOW_API_URL: ${MAILCOW_API_URL}
|
||||||
|
MAILCOW_API_KEY: ${MAILCOW_API_KEY}
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
WEBHOOK_SIGNAL_SECRET: ${WEBHOOK_SIGNAL_SECRET}
|
||||||
|
AUTH_SECRET: ${AUTH_SECRET}
|
||||||
|
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
# 3. Ayris Worker (Mail İzleyici)
|
||||||
|
worker:
|
||||||
|
build:
|
||||||
|
context: ../ayristech-worker
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ayrismail-worker
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/lib/docker/volumes/mailcowdockerized_vmail-vol-1/_data:/var/lib/docker/volumes/mailcowdockerized_vmail-vol-1/_data:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
NEXTJS_WEBHOOK_URL: "http://app:3000/api/webhooks/mail-signal"
|
||||||
|
WEBHOOK_SECRET: ${WEBHOOK_SIGNAL_SECRET}
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
35
lib/whatsapp.ts
Normal file
35
lib/whatsapp.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* lib/whatsapp.ts
|
||||||
|
* Centralized service to send WhatsApp messages via AyrisTech WhatsApp Worker.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function sendWA(to: string, message: string, userId: string = 'mustafa_yildiz') {
|
||||||
|
try {
|
||||||
|
const workerUrl = process.env.WHATSAPP_WORKER_URL;
|
||||||
|
const secret = process.env.WHATSAPP_SECRET;
|
||||||
|
|
||||||
|
if (!workerUrl || !secret) {
|
||||||
|
console.error('[WhatsApp] Missing environment variables (URL or Secret)');
|
||||||
|
return { success: false, error: 'Config missing' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/send-message`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
secret,
|
||||||
|
userId,
|
||||||
|
to,
|
||||||
|
message
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) throw new Error(data.error || 'Mesaj gönderilemedi');
|
||||||
|
|
||||||
|
return { success: true, data };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WhatsApp Error:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Bilinmeyen hata' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ model User {
|
|||||||
role String @default("DOMAIN_ADMIN") // SUPER_ADMIN or DOMAIN_ADMIN
|
role String @default("DOMAIN_ADMIN") // SUPER_ADMIN or DOMAIN_ADMIN
|
||||||
domains String[] @default([]) // ["*"] or list of domains
|
domains String[] @default([]) // ["*"] or list of domains
|
||||||
telegramId String?
|
telegramId String?
|
||||||
|
telegramEnabled Boolean @default(true)
|
||||||
|
whatsappNumber String?
|
||||||
|
whatsappEnabled Boolean @default(false)
|
||||||
mailboxMappings MailboxMapping[]
|
mailboxMappings MailboxMapping[]
|
||||||
notificationConfigs NotificationConfig[]
|
notificationConfigs NotificationConfig[]
|
||||||
notificationLogs NotificationLog[]
|
notificationLogs NotificationLog[]
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { prisma } from "../lib/prisma";
|
|
||||||
|
|
||||||
async function checkDb() {
|
|
||||||
try {
|
|
||||||
console.log("--- Notification Logs ---");
|
|
||||||
const nLogs = await prisma.notificationLog.findMany({
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
take: 5
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify(nLogs, null, 2));
|
|
||||||
|
|
||||||
console.log("\n--- System Logs ---");
|
|
||||||
const sLogs = await prisma.systemLog.findMany({
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
take: 5
|
|
||||||
});
|
|
||||||
console.log(JSON.stringify(sLogs, null, 2));
|
|
||||||
|
|
||||||
console.log("\n--- Mailbox Mappings ---");
|
|
||||||
const mappings = await prisma.mailboxMapping.findMany();
|
|
||||||
console.log(JSON.stringify(mappings, null, 2));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("DB Check failed:", error);
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDb();
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Client } from 'pg';
|
|
||||||
import 'dotenv/config';
|
|
||||||
|
|
||||||
async function testConnection() {
|
|
||||||
const connectionString = process.env.DATABASE_URL;
|
|
||||||
const client = new Client({ connectionString });
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("Connecting to:", connectionString?.split('@')[1]);
|
|
||||||
await client.connect();
|
|
||||||
console.log("Connected successfully!");
|
|
||||||
const res = await client.query('SELECT NOW()');
|
|
||||||
console.log("Current time from DB:", res.rows[0]);
|
|
||||||
await client.end();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Connection error:", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testConnection();
|
|
||||||
Reference in New Issue
Block a user