İlk sürüm: hDiyanetProxy v1.0.0
- Backend: Node.js + Express + MySQL + JWT auth - 8 MySQL tablosu (users, countries, states, cities, prayer_times, ramadan_times, eid_times, fetch_logs) - Diyanet API entegrasyonu (auth + token yönetimi) - Tüm API endpointleri (places, prayer-times, ramadan, eid, admin) - Rate limiting, CORS, input validation - Cron job (gece 02:00 otomatik veri çekme) - Frontend: Login, Dashboard, Fetch Panel, Namaz Vakitleri, Ramazan, Admin, Profil - Admin kullanıcı: admin/admin123
This commit is contained in:
21
.env.example
Normal file
21
.env.example
Normal file
@@ -0,0 +1,21 @@
|
||||
# hDiyanetProxy - Environment Variables Example
|
||||
|
||||
# MySQL Veritabanı
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=hdiyanetproxy
|
||||
DB_USER=root
|
||||
DB_PASS=your_password
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=change_this_to_a_random_secret_key
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Diyanet API
|
||||
DIYANET_API_URL=https://awqatsalah.diyanet.gov.tr
|
||||
DIYANET_EMAIL=your_diyanet_email
|
||||
DIYANET_PASSWORD=your_diyanet_password
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.env
|
||||
*.log
|
||||
node_modules/
|
||||
.opencode/
|
||||
176
README.md
Normal file
176
README.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# hDiyanetProxy
|
||||
|
||||
Diyanet İşleri Başkanlığı namaz vakitleri API'sinden veri çeken, yerel veritabanında saklayan ve güvenli API üzerinden sunan proxy uygulaması.
|
||||
|
||||
## Özellikler
|
||||
|
||||
- 🕌 Namaz vakitleri sorgulama (yıllık, günlük)
|
||||
- 🌙 Ramazan imsakiyesi ve iftar vakitleri
|
||||
- 🎉 Bayram namaz vakitleri
|
||||
- 📍 Ülke/İl/Şehir yer bilgileri yönetimi
|
||||
- 🔐 JWT tabanlı kimlik doğrulama
|
||||
- 👥 Kullanıcı yönetimi (admin paneli)
|
||||
- ⏰ Cron job ile otomatik veri güncelleme (gece 02:00)
|
||||
- 📊 Dashboard ve veri çekme logları
|
||||
- 📥 Manuel veri çekme paneli
|
||||
- 📤 CSV/JSON dışa aktarma
|
||||
|
||||
## Teknoloji
|
||||
|
||||
- **Backend:** Node.js + Express.js
|
||||
- **Veritabanı:** MySQL (mysql2)
|
||||
- **Auth:** JWT (jsonwebtoken + bcrypt)
|
||||
- **Frontend:** Vanilla HTML/CSS/JS
|
||||
- **Kaynak API:** [Diyanet Awqat Salah](https://awqatsalah.diyanet.gov.tr)
|
||||
|
||||
## Kurulum
|
||||
|
||||
### 1. Bağımlılıkları Kur
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Ortam Değişkenleri
|
||||
|
||||
`.env.example` dosyasını `.env` olarak kopyalayın ve değerlerini güncelleyin:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# .env dosyasını düzenleyin
|
||||
```
|
||||
|
||||
Ayrıca backend dizininde de `.env` gereklidir:
|
||||
```bash
|
||||
cp .env backend/.env
|
||||
```
|
||||
|
||||
### 3. Veritabanı Migration
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm run migrate
|
||||
```
|
||||
|
||||
### 4. İlk Admin Kullanıcı
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm run seed
|
||||
```
|
||||
|
||||
Varsayılan giriş: `admin` / `admin123`
|
||||
|
||||
### 5. Sunucuyu Başlat
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm start
|
||||
# veya geliştirme modu:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Uygulama `http://localhost:3000` adresinde çalışır.
|
||||
|
||||
## API Endpointleri
|
||||
|
||||
### Auth
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| POST | `/api/v1/auth/login` | Giriş yap, JWT token al |
|
||||
| POST | `/api/v1/auth/register` | Yeni kullanıcı kaydı |
|
||||
| GET | `/api/v1/auth/profile` | Profil bilgisi |
|
||||
| POST | `/api/v1/auth/refresh` | Token yenile |
|
||||
| POST | `/api/v1/auth/change-password` | Şifre değiştir |
|
||||
|
||||
### Yer Bilgileri
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| GET | `/api/v1/places/countries` | Ülkeleri listele |
|
||||
| GET | `/api/v1/places/states?countryId={id}` | Eyaletleri listele |
|
||||
| GET | `/api/v1/places/cities?stateId={id}` | Şehirleri listele |
|
||||
| GET | `/api/v1/places/city/{cityId}` | Şehir detayı |
|
||||
|
||||
### Namaz Vakitleri
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| GET | `/api/v1/prayer-times?cityId={id}&startDate=&endDate=` | Tarih aralığı |
|
||||
| GET | `/api/v1/prayer-times/today?cityId={id}` | Bugünkü vakit |
|
||||
|
||||
### Ramazan
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| GET | `/api/v1/ramadan?cityId={id}` | Ramazan takvimi |
|
||||
| GET | `/api/v1/ramadan/today?cityId={id}` | Bugünkü iftar |
|
||||
|
||||
### Bayram
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| GET | `/api/v1/eid?cityId={id}` | Bayram vakitleri |
|
||||
|
||||
### Admin (Admin yetkisi gerekli)
|
||||
| Method | Endpoint | Açıklama |
|
||||
|--------|----------|----------|
|
||||
| GET | `/api/v1/admin/dashboard` | İstatistikler |
|
||||
| POST | `/api/v1/admin/fetch/places` | Yer bilgilerini çek |
|
||||
| POST | `/api/v1/admin/fetch/yearly` | Yıllık namaz vakitlerini çek |
|
||||
| POST | `/api/v1/admin/fetch/ramadan` | Ramazan vakitlerini çek |
|
||||
| POST | `/api/v1/admin/fetch/eid` | Bayram vakitlerini çek |
|
||||
| POST | `/api/v1/admin/cache/clear` | Diyanet API cache temizle |
|
||||
| GET | `/api/v1/admin/logs` | Veri çekme logları |
|
||||
| GET | `/api/v1/admin/users` | Kullanıcı listesi |
|
||||
| POST | `/api/v1/admin/users` | Kullanıcı oluştur |
|
||||
| PATCH | `/api/v1/admin/users/:id/toggle` | Aktif/Pasif yap |
|
||||
| DELETE | `/api/v1/admin/users/:id` | Kullanıcı sil |
|
||||
|
||||
### Örnek Kullanım
|
||||
|
||||
```bash
|
||||
# Giriş yap
|
||||
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}' | jq -r '.token')
|
||||
|
||||
# Bugünkü namaz vakti
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
http://localhost:3000/api/v1/prayer-times/today?cityId=9541
|
||||
```
|
||||
|
||||
## Güvenlik
|
||||
|
||||
- Şifreler bcrypt ile hash'lenir
|
||||
- JWT token süresi: 24 saat
|
||||
- Rate limiting: 120 istek/dakika (genel), 20 istek/15dk (login)
|
||||
- CORS koruması aktif
|
||||
- Input validation (express-validator)
|
||||
|
||||
## Dosya Yapısı
|
||||
|
||||
```
|
||||
hDiyanetProxy/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── controllers/ # Route handler'ları
|
||||
│ │ ├── services/ # İş mantığı servisleri
|
||||
│ │ ├── models/ # Veritabanı modelleri
|
||||
│ │ ├── routes/ # Express route tanımları
|
||||
│ │ ├── middleware/ # Auth middleware
|
||||
│ │ ├── jobs/ # Cron job'lar
|
||||
│ │ └── utils/ # DB bağlantısı, migration
|
||||
│ ├── .env
|
||||
│ └── package.json
|
||||
├── frontend/
|
||||
│ └── src/
|
||||
│ ├── pages/ # HTML sayfaları
|
||||
│ ├── components/ # Sidebar vb. bileşenler
|
||||
│ └── services/ # API çağrıları
|
||||
├── .env
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Lisans
|
||||
|
||||
ISC
|
||||
240
REQUIREMENTS.md
Normal file
240
REQUIREMENTS.md
Normal file
@@ -0,0 +1,240 @@
|
||||
## hDiyanetProxy Projesi - AI Agent Prompt'u
|
||||
|
||||
---
|
||||
|
||||
**Proje Adı:** hDiyanetProxy
|
||||
|
||||
**Görev:** Aşağıdaki gereksinimlere göre tam kapsamlı bir web uygulaması ve API servisi geliştir.
|
||||
|
||||
---
|
||||
|
||||
### 📌 GENEL TANIM
|
||||
|
||||
Türkiye Diyanet İşleri Başkanlığı'nın namaz vakitleri API'sinden (`https://awqatsalah.diyanet.gov.tr`) veri çeken, bu verileri yerel veritabanında saklayan ve dışarıya kendi güvenli API'si üzerinden sunan **hDiyanetProxy** adlı bir uygulama geliştir.
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ TEKNOLOJİ YIĞINI
|
||||
|
||||
- **Backend:** Node.js + Express.js (veya Python + FastAPI — tercihe bırak)
|
||||
- **Veritabanı:** PostgreSQL (ya da SQLite geliştirme ortamı için)
|
||||
- **Auth:** JWT tabanlı token sistemi
|
||||
- **Frontend:** Sade, modern bir web arayüzü (HTML/CSS/JS veya React)
|
||||
- **Ortam:** Docker Compose ile ayağa kalkabilir yapı (opsiyonel ama önerilir)
|
||||
|
||||
---
|
||||
|
||||
### 🔌 KAYNAK API BİLGİLERİ
|
||||
|
||||
**Base URL:** `https://awqatsalah.diyanet.gov.tr`
|
||||
|
||||
Kullanılacak endpointler:
|
||||
|
||||
#### Namaz Vakitleri
|
||||
```
|
||||
POST /api/AwqatSalah/Yearly
|
||||
Body:
|
||||
{
|
||||
"cityId": {cityId},
|
||||
"startDate": "2026-01-01T00:00:00.0Z",
|
||||
"endDate": "2026-12-31T23:59:59.0Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Ramazan İmsakiyesi
|
||||
```
|
||||
GET /api/AwqatSalah/Ramadan/{cityId}
|
||||
```
|
||||
|
||||
#### Bayram Vakti ve Bayram Namazı
|
||||
```
|
||||
GET /api/AwqatSalah/Eid/{cityId}
|
||||
```
|
||||
|
||||
#### Yer Bilgileri
|
||||
```
|
||||
GET /api/Place/Countries
|
||||
GET /api/Place/States
|
||||
GET /api/Place/States/{countryId}
|
||||
GET /api/Place/Cities
|
||||
GET /api/Place/Cities/{stateId}
|
||||
GET /api/Place/CityDetail/{cityId}
|
||||
```
|
||||
|
||||
#### Cache Temizleme
|
||||
```
|
||||
GET /api/Cache/ClearCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🗄️ VERİTABANI YAPISI
|
||||
|
||||
Aşağıdaki tabloları oluştur:
|
||||
|
||||
1. **users** — Kullanıcı adı, şifre (hash'li), rol, oluşturulma tarihi
|
||||
2. **countries** — Ülke id, adı
|
||||
3. **states** — İl/Eyalet id, adı, ülke foreign key
|
||||
4. **cities** — Şehir id, adı, eyalet foreign key, koordinatlar
|
||||
5. **prayer_times** — cityId, tarih, imsak, güneş, öğle, ikindi, akşam, yatsı, kıble vakti
|
||||
6. **ramadan_times** — cityId, tarih, imsak, iftar, sahur saati gibi alanlar
|
||||
7. **eid_times** — cityId, bayram adı, tarih, namaz saati
|
||||
8. **fetch_logs** — Ne zaman, kim tarafından, hangi şehir için veri çekildiği logu
|
||||
|
||||
---
|
||||
|
||||
### ⚙️ BACKEND FONKSİYONELLİK
|
||||
|
||||
#### 1. Veri Çekme Servisi
|
||||
- Manuel tetikleme: Kullanıcı arayüzden şehir ve tarih aralığı seçip "Veri Çek" butonuna basar
|
||||
- Zamanlanmış çekme: Cron job ile belirli aralıklarla otomatik çekme (örn. her gün gece 02:00)
|
||||
- Çekilen veriler önce kontrol edilir; aynı şehir + tarih için kayıt varsa güncellenir, yoksa eklenir (upsert)
|
||||
- Yer bilgileri (ülke, eyalet, şehir) ayrı tablolarda saklanır ve periyodik olarak güncellenir
|
||||
|
||||
#### 2. Kimlik Doğrulama
|
||||
- `POST /auth/register` — Yeni kullanıcı kaydı (admin onaylı veya açık, tercihe göre)
|
||||
- `POST /auth/login` — Kullanıcı adı + şifre ile giriş, JWT token döner
|
||||
- Token süresi: 24 saat (refresh token desteği eklenebilir)
|
||||
- Tüm veri endpointleri `Authorization: Bearer {token}` header'ı zorunlu tutar
|
||||
|
||||
#### 3. Cache Yönetimi
|
||||
- Kaynak API'nin cache'ini temizlemek için `/api/Cache/ClearCache` çağrısını proxy eden bir endpoint yaz
|
||||
- Yerel cache için Redis entegrasyonu eklenebilir (opsiyonel)
|
||||
|
||||
---
|
||||
|
||||
### 🌐 hDiyanetProxy API ENDPOINTLERİ
|
||||
|
||||
Token ile korunan aşağıdaki endpointleri oluştur:
|
||||
|
||||
```
|
||||
# Auth
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/register
|
||||
|
||||
# Yer Bilgileri
|
||||
GET /api/v1/places/countries
|
||||
GET /api/v1/places/states?countryId={id}
|
||||
GET /api/v1/places/cities?stateId={id}
|
||||
GET /api/v1/places/city/{cityId}
|
||||
|
||||
# Namaz Vakitleri
|
||||
GET /api/v1/prayer-times?cityId={id}&startDate={date}&endDate={date}
|
||||
GET /api/v1/prayer-times/today?cityId={id}
|
||||
|
||||
# Ramazan / İftar
|
||||
GET /api/v1/ramadan?cityId={id}
|
||||
GET /api/v1/ramadan/today?cityId={id}
|
||||
|
||||
# Bayram
|
||||
GET /api/v1/eid?cityId={id}
|
||||
|
||||
# Veri Çekme (Admin)
|
||||
POST /api/v1/admin/fetch/yearly Body: { cityId, startDate, endDate }
|
||||
POST /api/v1/admin/fetch/ramadan Body: { cityId }
|
||||
POST /api/v1/admin/fetch/eid Body: { cityId }
|
||||
POST /api/v1/admin/fetch/places (Tüm yer bilgilerini günceller)
|
||||
|
||||
# Cache
|
||||
POST /api/v1/admin/cache/clear
|
||||
|
||||
# Loglar
|
||||
GET /api/v1/admin/logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ WEB ARAYÜZÜ
|
||||
|
||||
Aşağıdaki sayfaları içeren sade ve işlevsel bir web arayüzü oluştur:
|
||||
|
||||
#### 1. Giriş Sayfası (`/login`)
|
||||
- Kullanıcı adı ve şifre formu
|
||||
- Başarılı girişte token'ı localStorage'a kaydet
|
||||
- Hata mesajlarını göster
|
||||
|
||||
#### 2. Dashboard (`/dashboard`)
|
||||
- Sistemdeki toplam kayıt sayıları (şehir, namaz vakti, ramazan kaydı)
|
||||
- Son veri çekme logları tablosu
|
||||
- Hızlı veri sorgulama alanı
|
||||
|
||||
#### 3. Veri Çekme Paneli (`/fetch`)
|
||||
- Ülke → Eyalet → Şehir dropdown seçimi (dinamik, birbirini tetikleyen)
|
||||
- Tarih aralığı seçici
|
||||
- "Yıllık Namaz Vakitlerini Çek", "Ramazan Verilerini Çek", "Bayram Verilerini Çek" butonları
|
||||
- İşlem sonucunu canlı gösteren log/konsol alanı
|
||||
|
||||
#### 4. Namaz Vakitleri Sorgulama (`/prayer-times`)
|
||||
- Şehir ve tarih aralığı seçimi
|
||||
- Tablo şeklinde listeleme
|
||||
- CSV/JSON dışa aktarma butonu
|
||||
|
||||
#### 5. Ramazan / İftar Vakitleri (`/ramadan`)
|
||||
- Şehir seçimi
|
||||
- Ramazan takvimi tablo görünümü
|
||||
- İftar vakti vurgulu satır tasarımı
|
||||
|
||||
#### 6. Kullanıcı Yönetimi (`/admin/users`) — Sadece admin rolü
|
||||
- Kullanıcı listeleme, ekleme, pasif etme
|
||||
|
||||
#### 7. API Token Bilgisi (`/profile`)
|
||||
- Mevcut token'ı göster
|
||||
- Yeni token üret butonu
|
||||
- API kullanım dokümantasyonu linki
|
||||
|
||||
---
|
||||
|
||||
### 🔐 GÜVENLİK GEREKSİNİMLERİ
|
||||
|
||||
- Şifreler `bcrypt` ile hash'lenmeli
|
||||
- JWT secret `.env` dosyasında saklanmalı
|
||||
- Rate limiting eklenmeli (örn. dakikada 60 istek)
|
||||
- CORS ayarları yapılmalı
|
||||
- Input validasyonu tüm endpointlerde olmalı
|
||||
- `.env.example` dosyası oluşturulmalı
|
||||
|
||||
---
|
||||
|
||||
### 📁 PROJE DOSYA YAPISI
|
||||
|
||||
```
|
||||
hDiyanetProxy/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── controllers/
|
||||
│ │ ├── services/
|
||||
│ │ ├── models/
|
||||
│ │ ├── routes/
|
||||
│ │ ├── middleware/
|
||||
│ │ ├── jobs/ (cron jobs)
|
||||
│ │ └── utils/
|
||||
│ ├── .env.example
|
||||
│ └── package.json
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── pages/
|
||||
│ │ ├── components/
|
||||
│ │ └── services/ (API çağrıları)
|
||||
│ └── package.json
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📋 TAMAMLANMA KRİTERLERİ
|
||||
|
||||
- [ ] Kaynak Diyanet API'sinden başarıyla veri çekilebiliyor
|
||||
- [ ] Çekilen veriler veritabanına doğru şekilde kaydediliyor
|
||||
- [ ] JWT ile kullanıcı girişi ve token alma çalışıyor
|
||||
- [ ] Token ile korunan tüm endpointler çalışıyor
|
||||
- [ ] Web arayüzünden veri çekme ve sorgulama yapılabiliyor
|
||||
- [ ] Şehir listesi dinamik dropdown'larla yüklenebiliyor
|
||||
- [ ] Ramazan ve bayram verileri ayrı ayrı sorgulanabiliyor
|
||||
- [ ] Admin panelinden kullanıcı yönetimi yapılabiliyor
|
||||
- [ ] Tüm işlemler loglanıyor
|
||||
- [ ] README.md kurulum adımlarını içeriyor
|
||||
|
||||
---
|
||||
|
||||
Projeyi adım adım geliştir. Önce veritabanı şemasını ve backend iskeletini kur, ardından API endpointlerini yaz, en son web arayüzünü tamamla. Her adımda çalışır durumda kod üret.
|
||||
1905
backend/package-lock.json
generated
Normal file
1905
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
backend/package.json
Normal file
24
backend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "hdiyanetproxy-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "hDiyanetProxy Backend API",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js",
|
||||
"dev": "node --watch src/app.js",
|
||||
"migrate": "node src/utils/migrate.js",
|
||||
"seed": "node src/utils/seed.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "^3.18.2",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"express-validator": "^7.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"axios": "^1.7.7",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
98
backend/src/app.js
Normal file
98
backend/src/app.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// hDiyanetProxy - Ana Uygulama
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// --- Middleware ---
|
||||
|
||||
// CORS ayarları
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
// Body parser
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Rate limiting - dakikada 120 istek
|
||||
const limiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 dakika
|
||||
max: 120,
|
||||
message: { error: 'Çok fazla istek gönderdiniz, lütfen bir süre bekleyin' },
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Auth endpointleri için daha sıkı rate limiting
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 dakika
|
||||
max: 20,
|
||||
message: { error: 'Çok fazla giriş denemesi, 15 dakika bekleyin' }
|
||||
});
|
||||
app.use('/api/v1/auth/login', authLimiter);
|
||||
|
||||
// --- Frontend Statik Dosyalar ---
|
||||
app.use(express.static(path.join(__dirname, '../../frontend/src')));
|
||||
|
||||
// --- API Routes ---
|
||||
app.use('/api/v1/auth', require('./routes/auth'));
|
||||
app.use('/api/v1/places', require('./routes/places'));
|
||||
app.use('/api/v1/prayer-times', require('./routes/prayerTimes'));
|
||||
app.use('/api/v1/ramadan', require('./routes/ramadan'));
|
||||
app.use('/api/v1/eid', require('./routes/eid'));
|
||||
app.use('/api/v1/admin', require('./routes/admin'));
|
||||
|
||||
// --- API Durum Endpoint'i ---
|
||||
app.get('/api/v1/status', (req, res) => {
|
||||
res.json({
|
||||
name: 'hDiyanetProxy',
|
||||
version: '1.0.0',
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// --- Frontend SPA Route'ları ---
|
||||
app.get('/', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/login.html')));
|
||||
app.get('/login', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/login.html')));
|
||||
app.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/dashboard.html')));
|
||||
app.get('/fetch', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/fetch.html')));
|
||||
app.get('/prayer-times', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/prayer-times.html')));
|
||||
app.get('/ramadan', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/ramadan.html')));
|
||||
app.get('/admin/users', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/admin-users.html')));
|
||||
app.get('/profile', (req, res) => res.sendFile(path.join(__dirname, '../../frontend/src/pages/profile.html')));
|
||||
|
||||
// --- 404 Handler ---
|
||||
app.use((req, res) => {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(404).json({ error: 'Endpoint bulunamadı' });
|
||||
}
|
||||
res.status(404).send('Sayfa bulunamadı');
|
||||
});
|
||||
|
||||
// --- Error Handler ---
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Sunucu hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
});
|
||||
|
||||
// --- Sunucuyu Başlat ---
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 hDiyanetProxy sunucusu çalışıyor: http://localhost:${PORT}`);
|
||||
console.log(`📡 API: http://localhost:${PORT}/api/v1`);
|
||||
console.log(`🖥️ Frontend: http://localhost:${PORT}/\n`);
|
||||
|
||||
// Cron job'ları başlat
|
||||
const { startCronJobs } = require('./jobs/cronJobs');
|
||||
startCronJobs();
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
178
backend/src/controllers/adminController.js
Normal file
178
backend/src/controllers/adminController.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// hDiyanetProxy - Admin Controller
|
||||
const FetchService = require('../services/fetchService');
|
||||
const FetchLogModel = require('../models/FetchLog');
|
||||
const UserModel = require('../models/User');
|
||||
const PlaceModel = require('../models/Place');
|
||||
const DiyanetService = require('../services/diyanetService');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
const AdminController = {
|
||||
// Dashboard istatistikleri
|
||||
async getDashboard(req, res) {
|
||||
try {
|
||||
const stats = await PlaceModel.getStats();
|
||||
const recentLogs = await FetchLogModel.getRecent(10);
|
||||
res.json({ stats, recentLogs });
|
||||
} catch (err) {
|
||||
console.error('Dashboard hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Yer bilgilerini çek
|
||||
async fetchPlaces(req, res) {
|
||||
try {
|
||||
const result = await FetchService.fetchAndSavePlaces(req.user.id);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Fetch places hatası:', err);
|
||||
res.status(500).json({ error: `Yer bilgileri çekilirken hata: ${err.message}` });
|
||||
}
|
||||
},
|
||||
|
||||
// Yıllık namaz vakitlerini çek
|
||||
async fetchYearly(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { cityId, startDate, endDate } = req.body;
|
||||
const result = await FetchService.fetchAndSaveYearly(cityId, startDate, endDate, req.user.id);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Fetch yearly hatası:', err);
|
||||
res.status(500).json({ error: `Namaz vakitleri çekilirken hata: ${err.message}` });
|
||||
}
|
||||
},
|
||||
|
||||
// Ramazan vakitlerini çek
|
||||
async fetchRamadan(req, res) {
|
||||
try {
|
||||
const { cityId } = req.body;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId gerekli' });
|
||||
}
|
||||
const result = await FetchService.fetchAndSaveRamadan(cityId, req.user.id);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Fetch ramadan hatası:', err);
|
||||
res.status(500).json({ error: `Ramazan vakitleri çekilirken hata: ${err.message}` });
|
||||
}
|
||||
},
|
||||
|
||||
// Bayram vakitlerini çek
|
||||
async fetchEid(req, res) {
|
||||
try {
|
||||
const { cityId } = req.body;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId gerekli' });
|
||||
}
|
||||
const result = await FetchService.fetchAndSaveEid(cityId, req.user.id);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Fetch eid hatası:', err);
|
||||
res.status(500).json({ error: `Bayram vakitleri çekilirken hata: ${err.message}` });
|
||||
}
|
||||
},
|
||||
|
||||
// Cache temizle
|
||||
async clearCache(req, res) {
|
||||
try {
|
||||
const result = await DiyanetService.clearCache();
|
||||
await FetchLogModel.create(req.user.id, 'clear_cache', null, null, 'Diyanet API cache temizlendi', 'success', 0);
|
||||
res.json({ success: true, message: 'Cache temizlendi', data: result });
|
||||
} catch (err) {
|
||||
console.error('Clear cache hatası:', err);
|
||||
res.status(500).json({ error: `Cache temizlenirken hata: ${err.message}` });
|
||||
}
|
||||
},
|
||||
|
||||
// Logları getir
|
||||
async getLogs(req, res) {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const result = await FetchLogModel.getAll(page, limit);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('Logs hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// --- Kullanıcı Yönetimi ---
|
||||
|
||||
// Tüm kullanıcıları listele
|
||||
async getUsers(req, res) {
|
||||
try {
|
||||
const users = await UserModel.findAll();
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
console.error('Users hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Kullanıcı oluştur
|
||||
async createUser(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { username, password, role } = req.body;
|
||||
|
||||
const existing = await UserModel.findByUsername(username);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Bu kullanıcı adı zaten kullanılıyor' });
|
||||
}
|
||||
|
||||
const user = await UserModel.create(username, password, role || 'user');
|
||||
res.status(201).json({ message: 'Kullanıcı oluşturuldu', user });
|
||||
} catch (err) {
|
||||
console.error('Create user hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Kullanıcı aktif/pasif yap
|
||||
async toggleUserActive(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { isActive } = req.body;
|
||||
|
||||
// Kendini pasif yapamasın
|
||||
if (parseInt(id) === req.user.id) {
|
||||
return res.status(400).json({ error: 'Kendinizi pasif yapamazsınız' });
|
||||
}
|
||||
|
||||
await UserModel.toggleActive(parseInt(id), isActive);
|
||||
res.json({ message: isActive ? 'Kullanıcı aktif edildi' : 'Kullanıcı pasif edildi' });
|
||||
} catch (err) {
|
||||
console.error('Toggle user hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Kullanıcı sil
|
||||
async deleteUser(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (parseInt(id) === req.user.id) {
|
||||
return res.status(400).json({ error: 'Kendinizi silemezsiniz' });
|
||||
}
|
||||
|
||||
await UserModel.delete(parseInt(id));
|
||||
res.json({ message: 'Kullanıcı silindi' });
|
||||
} catch (err) {
|
||||
console.error('Delete user hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AdminController;
|
||||
136
backend/src/controllers/authController.js
Normal file
136
backend/src/controllers/authController.js
Normal file
@@ -0,0 +1,136 @@
|
||||
// hDiyanetProxy - Auth Controller
|
||||
const jwt = require('jsonwebtoken');
|
||||
const UserModel = require('../models/User');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
const AuthController = {
|
||||
// Kullanıcı girişi
|
||||
async login(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Kullanıcıyı bul
|
||||
const user = await UserModel.findByUsername(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Kullanıcı adı veya şifre hatalı' });
|
||||
}
|
||||
|
||||
// Aktif mi kontrol et
|
||||
if (!user.is_active) {
|
||||
return res.status(403).json({ error: 'Hesabınız pasif durumda' });
|
||||
}
|
||||
|
||||
// Şifreyi doğrula
|
||||
const isValid = await UserModel.verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: 'Kullanıcı adı veya şifre hatalı' });
|
||||
}
|
||||
|
||||
// JWT token oluştur
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: 'Giriş başarılı',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Login hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Kullanıcı kaydı
|
||||
async register(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Kullanıcı adı zaten var mı kontrol et
|
||||
const existing = await UserModel.findByUsername(username);
|
||||
if (existing) {
|
||||
return res.status(409).json({ error: 'Bu kullanıcı adı zaten kullanılıyor' });
|
||||
}
|
||||
|
||||
// Yeni kullanıcı oluştur
|
||||
const user = await UserModel.create(username, password, 'user');
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Kullanıcı başarıyla oluşturuldu',
|
||||
user: { id: user.id, username: user.username, role: user.role }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Register hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Profil bilgisi
|
||||
async profile(req, res) {
|
||||
try {
|
||||
const user = await UserModel.findById(req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Kullanıcı bulunamadı' });
|
||||
}
|
||||
res.json({ user });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Yeni token oluştur
|
||||
async refreshToken(req, res) {
|
||||
try {
|
||||
const token = jwt.sign(
|
||||
{ id: req.user.id, username: req.user.username, role: req.user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
res.json({ token });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Şifre değiştir
|
||||
async changePassword(req, res) {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const user = await UserModel.findByUsername(req.user.username);
|
||||
|
||||
const isValid = await UserModel.verifyPassword(currentPassword, user.password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: 'Mevcut şifre hatalı' });
|
||||
}
|
||||
|
||||
await UserModel.updatePassword(req.user.id, newPassword);
|
||||
res.json({ message: 'Şifre başarıyla güncellendi' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AuthController;
|
||||
21
backend/src/controllers/eidController.js
Normal file
21
backend/src/controllers/eidController.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// hDiyanetProxy - Eid Controller
|
||||
const EidModel = require('../models/EidTime');
|
||||
|
||||
const EidController = {
|
||||
// Şehre göre bayram vakitlerini getir
|
||||
async getByCityId(req, res) {
|
||||
try {
|
||||
const { cityId } = req.query;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId parametresi gerekli' });
|
||||
}
|
||||
const times = await EidModel.getByCityId(parseInt(cityId));
|
||||
res.json(times);
|
||||
} catch (err) {
|
||||
console.error('Eid hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EidController;
|
||||
62
backend/src/controllers/placesController.js
Normal file
62
backend/src/controllers/placesController.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// hDiyanetProxy - Places Controller
|
||||
const PlaceModel = require('../models/Place');
|
||||
|
||||
const PlacesController = {
|
||||
// Tüm ülkeleri getir
|
||||
async getCountries(req, res) {
|
||||
try {
|
||||
const countries = await PlaceModel.getAllCountries();
|
||||
res.json(countries);
|
||||
} catch (err) {
|
||||
console.error('Countries hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Ülkeye göre eyaletleri getir
|
||||
async getStates(req, res) {
|
||||
try {
|
||||
const { countryId } = req.query;
|
||||
if (!countryId) {
|
||||
return res.status(400).json({ error: 'countryId parametresi gerekli' });
|
||||
}
|
||||
const states = await PlaceModel.getStatesByCountry(parseInt(countryId));
|
||||
res.json(states);
|
||||
} catch (err) {
|
||||
console.error('States hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Eyalete göre şehirleri getir
|
||||
async getCities(req, res) {
|
||||
try {
|
||||
const { stateId } = req.query;
|
||||
if (!stateId) {
|
||||
return res.status(400).json({ error: 'stateId parametresi gerekli' });
|
||||
}
|
||||
const cities = await PlaceModel.getCitiesByState(parseInt(stateId));
|
||||
res.json(cities);
|
||||
} catch (err) {
|
||||
console.error('Cities hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Şehir detayı
|
||||
async getCityDetail(req, res) {
|
||||
try {
|
||||
const { cityId } = req.params;
|
||||
const city = await PlaceModel.getCityDetail(parseInt(cityId));
|
||||
if (!city) {
|
||||
return res.status(404).json({ error: 'Şehir bulunamadı' });
|
||||
}
|
||||
res.json(city);
|
||||
} catch (err) {
|
||||
console.error('City detail hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PlacesController;
|
||||
46
backend/src/controllers/prayerTimesController.js
Normal file
46
backend/src/controllers/prayerTimesController.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// hDiyanetProxy - Prayer Times Controller
|
||||
const PrayerTimeModel = require('../models/PrayerTime');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
const PrayerTimesController = {
|
||||
// Tarih aralığına göre namaz vakitlerini getir
|
||||
async getByDateRange(req, res) {
|
||||
try {
|
||||
const { cityId, startDate, endDate } = req.query;
|
||||
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId parametresi gerekli' });
|
||||
}
|
||||
|
||||
const start = startDate || new Date().toISOString().split('T')[0];
|
||||
const end = endDate || start;
|
||||
|
||||
const times = await PrayerTimeModel.getByDateRange(parseInt(cityId), start, end);
|
||||
res.json(times);
|
||||
} catch (err) {
|
||||
console.error('Prayer times hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Bugünkü namaz vaktini getir
|
||||
async getToday(req, res) {
|
||||
try {
|
||||
const { cityId } = req.query;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId parametresi gerekli' });
|
||||
}
|
||||
|
||||
const today = await PrayerTimeModel.getToday(parseInt(cityId));
|
||||
if (!today) {
|
||||
return res.status(404).json({ error: 'Bugün için namaz vakti bulunamadı' });
|
||||
}
|
||||
res.json(today);
|
||||
} catch (err) {
|
||||
console.error('Prayer times today hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PrayerTimesController;
|
||||
39
backend/src/controllers/ramadanController.js
Normal file
39
backend/src/controllers/ramadanController.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// hDiyanetProxy - Ramadan Controller
|
||||
const RamadanModel = require('../models/RamadanTime');
|
||||
|
||||
const RamadanController = {
|
||||
// Şehre göre ramazan vakitlerini getir
|
||||
async getByCityId(req, res) {
|
||||
try {
|
||||
const { cityId } = req.query;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId parametresi gerekli' });
|
||||
}
|
||||
const times = await RamadanModel.getByCityId(parseInt(cityId));
|
||||
res.json(times);
|
||||
} catch (err) {
|
||||
console.error('Ramadan hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
},
|
||||
|
||||
// Bugünkü ramazan vaktini getir
|
||||
async getToday(req, res) {
|
||||
try {
|
||||
const { cityId } = req.query;
|
||||
if (!cityId) {
|
||||
return res.status(400).json({ error: 'cityId parametresi gerekli' });
|
||||
}
|
||||
const today = await RamadanModel.getToday(parseInt(cityId));
|
||||
if (!today) {
|
||||
return res.status(404).json({ error: 'Bugün için ramazan vakti bulunamadı' });
|
||||
}
|
||||
res.json(today);
|
||||
} catch (err) {
|
||||
console.error('Ramadan today hatası:', err);
|
||||
res.status(500).json({ error: 'Sunucu hatası' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RamadanController;
|
||||
47
backend/src/jobs/cronJobs.js
Normal file
47
backend/src/jobs/cronJobs.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// hDiyanetProxy - Cron Jobs
|
||||
// Her gece 02:00'de otomatik veri çekme
|
||||
const cron = require('node-cron');
|
||||
const FetchService = require('../services/fetchService');
|
||||
const pool = require('../utils/db');
|
||||
|
||||
function startCronJobs() {
|
||||
// Her gece saat 02:00'de çalışır (Türkiye saati)
|
||||
cron.schedule('0 2 * * *', async () => {
|
||||
console.log('\n⏰ [CRON] Otomatik veri çekme başladı:', new Date().toISOString());
|
||||
|
||||
try {
|
||||
// Veritabanındaki tüm şehirler için günlük namaz vakitlerini güncelle
|
||||
const [cities] = await pool.execute('SELECT DISTINCT city_id FROM prayer_times LIMIT 50');
|
||||
|
||||
if (cities.length === 0) {
|
||||
console.log(' ℹ️ Güncellenecek şehir bulunamadı');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const startDate = `${currentYear}-01-01T00:00:00.0Z`;
|
||||
const endDate = `${currentYear}-12-31T23:59:59.0Z`;
|
||||
|
||||
for (const city of cities) {
|
||||
try {
|
||||
await FetchService.fetchAndSaveYearly(city.city_id, startDate, endDate, null);
|
||||
console.log(` ✅ Şehir ${city.city_id} güncellendi`);
|
||||
} catch (err) {
|
||||
console.error(` ❌ Şehir ${city.city_id} güncellenemedi: ${err.message}`);
|
||||
}
|
||||
// API'yi yormamak için bekleme
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
console.log('⏰ [CRON] Otomatik veri çekme tamamlandı\n');
|
||||
} catch (err) {
|
||||
console.error('⏰ [CRON] Hata:', err.message);
|
||||
}
|
||||
}, {
|
||||
timezone: 'Europe/Istanbul'
|
||||
});
|
||||
|
||||
console.log('⏰ Cron job ayarlandı: Her gece 02:00 (Europe/Istanbul)');
|
||||
}
|
||||
|
||||
module.exports = { startCronJobs };
|
||||
31
backend/src/middleware/auth.js
Normal file
31
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// hDiyanetProxy - JWT Auth Middleware
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Token doğrulama middleware'i
|
||||
function authMiddleware(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Yetkilendirme token\'ı gerekli' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Geçersiz veya süresi dolmuş token' });
|
||||
}
|
||||
}
|
||||
|
||||
// Admin rolü kontrolü middleware'i
|
||||
function adminMiddleware(req, res, next) {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Bu işlem için admin yetkisi gerekli' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { authMiddleware, adminMiddleware };
|
||||
51
backend/src/models/EidTime.js
Normal file
51
backend/src/models/EidTime.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// hDiyanetProxy - Bayram Vakitleri Modeli
|
||||
const pool = require('../utils/db');
|
||||
|
||||
const EidModel = {
|
||||
// Bayram vakti ekle veya güncelle
|
||||
async upsert(cityId, eidName, date, times) {
|
||||
await pool.execute(
|
||||
`INSERT INTO eid_times (city_id, eid_name, date, prayer_time, fajr, sunrise, dhuhr, asr, maghrib, isha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
prayer_time = VALUES(prayer_time), fajr = VALUES(fajr), sunrise = VALUES(sunrise),
|
||||
dhuhr = VALUES(dhuhr), asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha)`,
|
||||
[cityId, eidName, date, times.prayerTime, times.fajr, times.sunrise, times.dhuhr, times.asr, times.maghrib, times.isha]
|
||||
);
|
||||
},
|
||||
|
||||
// Şehre göre bayram vakitlerini getir
|
||||
async getByCityId(cityId) {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM eid_times WHERE city_id = ? ORDER BY date',
|
||||
[cityId]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// Toplu ekleme
|
||||
async bulkUpsert(cityId, items) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
for (const item of items) {
|
||||
await conn.execute(
|
||||
`INSERT INTO eid_times (city_id, eid_name, date, prayer_time, fajr, sunrise, dhuhr, asr, maghrib, isha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
prayer_time = VALUES(prayer_time), fajr = VALUES(fajr), sunrise = VALUES(sunrise),
|
||||
dhuhr = VALUES(dhuhr), asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha)`,
|
||||
[cityId, item.eidName, item.date, item.prayerTime, item.fajr, item.sunrise, item.dhuhr, item.asr, item.maghrib, item.isha]
|
||||
);
|
||||
}
|
||||
await conn.commit();
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EidModel;
|
||||
43
backend/src/models/FetchLog.js
Normal file
43
backend/src/models/FetchLog.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// hDiyanetProxy - Fetch Log Modeli
|
||||
const pool = require('../utils/db');
|
||||
|
||||
const FetchLogModel = {
|
||||
// Log kaydı oluştur
|
||||
async create(userId, action, cityId, cityName, details, status = 'success', recordsCount = 0) {
|
||||
const [result] = await pool.execute(
|
||||
'INSERT INTO fetch_logs (user_id, action, city_id, city_name, details, status, records_count) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[userId, action, cityId, cityName, details, status, recordsCount]
|
||||
);
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
// Son logları getir
|
||||
async getRecent(limit = 50) {
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT fl.*, u.username
|
||||
FROM fetch_logs fl
|
||||
LEFT JOIN users u ON fl.user_id = u.id
|
||||
ORDER BY fl.created_at DESC
|
||||
LIMIT ?`,
|
||||
[limit]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// Tüm logları getir (sayfalı)
|
||||
async getAll(page = 1, limit = 50) {
|
||||
const offset = (page - 1) * limit;
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT fl.*, u.username
|
||||
FROM fetch_logs fl
|
||||
LEFT JOIN users u ON fl.user_id = u.id
|
||||
ORDER BY fl.created_at DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[limit, offset]
|
||||
);
|
||||
const [[{ total }]] = await pool.execute('SELECT COUNT(*) as total FROM fetch_logs');
|
||||
return { logs: rows, total, page, limit };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FetchLogModel;
|
||||
80
backend/src/models/Place.js
Normal file
80
backend/src/models/Place.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// hDiyanetProxy - Yer Bilgisi Modeli
|
||||
const pool = require('../utils/db');
|
||||
|
||||
const PlaceModel = {
|
||||
// --- Ülkeler ---
|
||||
async upsertCountry(id, name) {
|
||||
await pool.execute(
|
||||
'INSERT INTO countries (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)',
|
||||
[id, name]
|
||||
);
|
||||
},
|
||||
|
||||
async getAllCountries() {
|
||||
const [rows] = await pool.execute('SELECT * FROM countries ORDER BY name');
|
||||
return rows;
|
||||
},
|
||||
|
||||
// --- Eyaletler / İller ---
|
||||
async upsertState(id, name, countryId) {
|
||||
await pool.execute(
|
||||
'INSERT INTO states (id, name, country_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)',
|
||||
[id, name, countryId]
|
||||
);
|
||||
},
|
||||
|
||||
async getStatesByCountry(countryId) {
|
||||
const [rows] = await pool.execute('SELECT * FROM states WHERE country_id = ? ORDER BY name', [countryId]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getAllStates() {
|
||||
const [rows] = await pool.execute('SELECT * FROM states ORDER BY name');
|
||||
return rows;
|
||||
},
|
||||
|
||||
// --- Şehirler / İlçeler ---
|
||||
async upsertCity(id, name, stateId, lat = null, lng = null) {
|
||||
await pool.execute(
|
||||
'INSERT INTO cities (id, name, state_id, latitude, longitude) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name), latitude = VALUES(latitude), longitude = VALUES(longitude)',
|
||||
[id, name, stateId, lat, lng]
|
||||
);
|
||||
},
|
||||
|
||||
async getCitiesByState(stateId) {
|
||||
const [rows] = await pool.execute('SELECT * FROM cities WHERE state_id = ? ORDER BY name', [stateId]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async getCityDetail(cityId) {
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT c.*, s.name as state_name, co.name as country_name
|
||||
FROM cities c
|
||||
JOIN states s ON c.state_id = s.id
|
||||
JOIN countries co ON s.country_id = co.id
|
||||
WHERE c.id = ?`,
|
||||
[cityId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// İstatistikler
|
||||
async getStats() {
|
||||
const [[countries]] = await pool.execute('SELECT COUNT(*) as count FROM countries');
|
||||
const [[states]] = await pool.execute('SELECT COUNT(*) as count FROM states');
|
||||
const [[cities]] = await pool.execute('SELECT COUNT(*) as count FROM cities');
|
||||
const [[prayerTimes]] = await pool.execute('SELECT COUNT(*) as count FROM prayer_times');
|
||||
const [[ramadanTimes]] = await pool.execute('SELECT COUNT(*) as count FROM ramadan_times');
|
||||
const [[eidTimes]] = await pool.execute('SELECT COUNT(*) as count FROM eid_times');
|
||||
return {
|
||||
countries: countries.count,
|
||||
states: states.count,
|
||||
cities: cities.count,
|
||||
prayerTimes: prayerTimes.count,
|
||||
ramadanTimes: ramadanTimes.count,
|
||||
eidTimes: eidTimes.count
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PlaceModel;
|
||||
61
backend/src/models/PrayerTime.js
Normal file
61
backend/src/models/PrayerTime.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// hDiyanetProxy - Namaz Vakitleri Modeli
|
||||
const pool = require('../utils/db');
|
||||
|
||||
const PrayerTimeModel = {
|
||||
// Namaz vakti ekle veya güncelle (upsert)
|
||||
async upsert(cityId, date, times) {
|
||||
await pool.execute(
|
||||
`INSERT INTO prayer_times (city_id, date, fajr, sunrise, dhuhr, asr, maghrib, isha, qibla)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fajr = VALUES(fajr), sunrise = VALUES(sunrise), dhuhr = VALUES(dhuhr),
|
||||
asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha), qibla = VALUES(qibla)`,
|
||||
[cityId, date, times.fajr, times.sunrise, times.dhuhr, times.asr, times.maghrib, times.isha, times.qibla || null]
|
||||
);
|
||||
},
|
||||
|
||||
// Tarih aralığına göre namaz vakitlerini getir
|
||||
async getByDateRange(cityId, startDate, endDate) {
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT * FROM prayer_times WHERE city_id = ? AND date BETWEEN ? AND ? ORDER BY date`,
|
||||
[cityId, startDate, endDate]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// Bugünkü namaz vaktini getir
|
||||
async getToday(cityId) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM prayer_times WHERE city_id = ? AND date = ?',
|
||||
[cityId, today]
|
||||
);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// Toplu ekleme (batch upsert)
|
||||
async bulkUpsert(cityId, timesArray) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
for (const item of timesArray) {
|
||||
await conn.execute(
|
||||
`INSERT INTO prayer_times (city_id, date, fajr, sunrise, dhuhr, asr, maghrib, isha, qibla)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fajr = VALUES(fajr), sunrise = VALUES(sunrise), dhuhr = VALUES(dhuhr),
|
||||
asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha), qibla = VALUES(qibla)`,
|
||||
[cityId, item.date, item.fajr, item.sunrise, item.dhuhr, item.asr, item.maghrib, item.isha, item.qibla || null]
|
||||
);
|
||||
}
|
||||
await conn.commit();
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PrayerTimeModel;
|
||||
61
backend/src/models/RamadanTime.js
Normal file
61
backend/src/models/RamadanTime.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// hDiyanetProxy - Ramazan Vakitleri Modeli
|
||||
const pool = require('../utils/db');
|
||||
|
||||
const RamadanModel = {
|
||||
// Ramazan vakti ekle veya güncelle
|
||||
async upsert(cityId, date, times) {
|
||||
await pool.execute(
|
||||
`INSERT INTO ramadan_times (city_id, date, fajr, sunrise, dhuhr, asr, maghrib, isha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fajr = VALUES(fajr), sunrise = VALUES(sunrise), dhuhr = VALUES(dhuhr),
|
||||
asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha)`,
|
||||
[cityId, date, times.fajr, times.sunrise, times.dhuhr, times.asr, times.maghrib, times.isha]
|
||||
);
|
||||
},
|
||||
|
||||
// Şehre göre ramazan vakitlerini getir
|
||||
async getByCityId(cityId) {
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM ramadan_times WHERE city_id = ? ORDER BY date',
|
||||
[cityId]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// Bugünkü ramazan vaktini getir
|
||||
async getToday(cityId) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const [rows] = await pool.execute(
|
||||
'SELECT * FROM ramadan_times WHERE city_id = ? AND date = ?',
|
||||
[cityId, today]
|
||||
);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// Toplu ekleme
|
||||
async bulkUpsert(cityId, timesArray) {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
for (const item of timesArray) {
|
||||
await conn.execute(
|
||||
`INSERT INTO ramadan_times (city_id, date, fajr, sunrise, dhuhr, asr, maghrib, isha)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fajr = VALUES(fajr), sunrise = VALUES(sunrise), dhuhr = VALUES(dhuhr),
|
||||
asr = VALUES(asr), maghrib = VALUES(maghrib), isha = VALUES(isha)`,
|
||||
[cityId, item.date, item.fajr, item.sunrise, item.dhuhr, item.asr, item.maghrib, item.isha]
|
||||
);
|
||||
}
|
||||
await conn.commit();
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RamadanModel;
|
||||
56
backend/src/models/User.js
Normal file
56
backend/src/models/User.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// hDiyanetProxy - Kullanıcı Modeli
|
||||
const pool = require('../utils/db');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const UserModel = {
|
||||
// Kullanıcı adına göre bul
|
||||
async findByUsername(username) {
|
||||
const [rows] = await pool.execute('SELECT * FROM users WHERE username = ?', [username]);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// ID'ye göre bul
|
||||
async findById(id) {
|
||||
const [rows] = await pool.execute('SELECT id, username, role, is_active, created_at FROM users WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// Yeni kullanıcı oluştur
|
||||
async create(username, password, role = 'user') {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const [result] = await pool.execute(
|
||||
'INSERT INTO users (username, password, role) VALUES (?, ?, ?)',
|
||||
[username, hashedPassword, role]
|
||||
);
|
||||
return { id: result.insertId, username, role };
|
||||
},
|
||||
|
||||
// Şifre doğrula
|
||||
async verifyPassword(plainPassword, hashedPassword) {
|
||||
return bcrypt.compare(plainPassword, hashedPassword);
|
||||
},
|
||||
|
||||
// Tüm kullanıcıları listele
|
||||
async findAll() {
|
||||
const [rows] = await pool.execute('SELECT id, username, role, is_active, created_at, updated_at FROM users ORDER BY id');
|
||||
return rows;
|
||||
},
|
||||
|
||||
// Kullanıcıyı aktif/pasif yap
|
||||
async toggleActive(id, isActive) {
|
||||
await pool.execute('UPDATE users SET is_active = ? WHERE id = ?', [isActive ? 1 : 0, id]);
|
||||
},
|
||||
|
||||
// Kullanıcı sil
|
||||
async delete(id) {
|
||||
await pool.execute('DELETE FROM users WHERE id = ?', [id]);
|
||||
},
|
||||
|
||||
// Şifre güncelle
|
||||
async updatePassword(id, newPassword) {
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await pool.execute('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = UserModel;
|
||||
55
backend/src/routes/admin.js
Normal file
55
backend/src/routes/admin.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// hDiyanetProxy - Admin Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body } = require('express-validator');
|
||||
const AdminController = require('../controllers/adminController');
|
||||
const { authMiddleware, adminMiddleware } = require('../middleware/auth');
|
||||
|
||||
// Tüm admin endpointleri auth + admin yetkisi gerektirir
|
||||
router.use(authMiddleware);
|
||||
router.use(adminMiddleware);
|
||||
|
||||
// GET /api/v1/admin/dashboard
|
||||
router.get('/dashboard', AdminController.getDashboard);
|
||||
|
||||
// POST /api/v1/admin/fetch/places
|
||||
router.post('/fetch/places', AdminController.fetchPlaces);
|
||||
|
||||
// POST /api/v1/admin/fetch/yearly
|
||||
router.post('/fetch/yearly', [
|
||||
body('cityId').isInt().withMessage('cityId sayı olmalı'),
|
||||
body('startDate').notEmpty().withMessage('startDate gerekli'),
|
||||
body('endDate').notEmpty().withMessage('endDate gerekli')
|
||||
], AdminController.fetchYearly);
|
||||
|
||||
// POST /api/v1/admin/fetch/ramadan
|
||||
router.post('/fetch/ramadan', AdminController.fetchRamadan);
|
||||
|
||||
// POST /api/v1/admin/fetch/eid
|
||||
router.post('/fetch/eid', AdminController.fetchEid);
|
||||
|
||||
// POST /api/v1/admin/cache/clear
|
||||
router.post('/cache/clear', AdminController.clearCache);
|
||||
|
||||
// GET /api/v1/admin/logs
|
||||
router.get('/logs', AdminController.getLogs);
|
||||
|
||||
// --- Kullanıcı Yönetimi ---
|
||||
|
||||
// GET /api/v1/admin/users
|
||||
router.get('/users', AdminController.getUsers);
|
||||
|
||||
// POST /api/v1/admin/users
|
||||
router.post('/users', [
|
||||
body('username').trim().isLength({ min: 3, max: 50 }).withMessage('Kullanıcı adı 3-50 karakter olmalı'),
|
||||
body('password').isLength({ min: 6 }).withMessage('Şifre en az 6 karakter olmalı'),
|
||||
body('role').optional().isIn(['admin', 'user']).withMessage('Rol admin veya user olmalı')
|
||||
], AdminController.createUser);
|
||||
|
||||
// PATCH /api/v1/admin/users/:id/toggle
|
||||
router.patch('/users/:id/toggle', AdminController.toggleUserActive);
|
||||
|
||||
// DELETE /api/v1/admin/users/:id
|
||||
router.delete('/users/:id', AdminController.deleteUser);
|
||||
|
||||
module.exports = router;
|
||||
32
backend/src/routes/auth.js
Normal file
32
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// hDiyanetProxy - Auth Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { body } = require('express-validator');
|
||||
const AuthController = require('../controllers/authController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
// POST /api/v1/auth/login
|
||||
router.post('/login', [
|
||||
body('username').trim().notEmpty().withMessage('Kullanıcı adı gerekli'),
|
||||
body('password').notEmpty().withMessage('Şifre gerekli')
|
||||
], AuthController.login);
|
||||
|
||||
// POST /api/v1/auth/register
|
||||
router.post('/register', [
|
||||
body('username').trim().isLength({ min: 3, max: 50 }).withMessage('Kullanıcı adı 3-50 karakter olmalı'),
|
||||
body('password').isLength({ min: 6 }).withMessage('Şifre en az 6 karakter olmalı')
|
||||
], AuthController.register);
|
||||
|
||||
// GET /api/v1/auth/profile
|
||||
router.get('/profile', authMiddleware, AuthController.profile);
|
||||
|
||||
// POST /api/v1/auth/refresh
|
||||
router.post('/refresh', authMiddleware, AuthController.refreshToken);
|
||||
|
||||
// POST /api/v1/auth/change-password
|
||||
router.post('/change-password', authMiddleware, [
|
||||
body('currentPassword').notEmpty().withMessage('Mevcut şifre gerekli'),
|
||||
body('newPassword').isLength({ min: 6 }).withMessage('Yeni şifre en az 6 karakter olmalı')
|
||||
], AuthController.changePassword);
|
||||
|
||||
module.exports = router;
|
||||
12
backend/src/routes/eid.js
Normal file
12
backend/src/routes/eid.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// hDiyanetProxy - Eid Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const EidController = require('../controllers/eidController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// GET /api/v1/eid?cityId={id}
|
||||
router.get('/', EidController.getByCityId);
|
||||
|
||||
module.exports = router;
|
||||
22
backend/src/routes/places.js
Normal file
22
backend/src/routes/places.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// hDiyanetProxy - Places Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const PlacesController = require('../controllers/placesController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
// Tüm endpointler auth gerektirir
|
||||
router.use(authMiddleware);
|
||||
|
||||
// GET /api/v1/places/countries
|
||||
router.get('/countries', PlacesController.getCountries);
|
||||
|
||||
// GET /api/v1/places/states?countryId={id}
|
||||
router.get('/states', PlacesController.getStates);
|
||||
|
||||
// GET /api/v1/places/cities?stateId={id}
|
||||
router.get('/cities', PlacesController.getCities);
|
||||
|
||||
// GET /api/v1/places/city/:cityId
|
||||
router.get('/city/:cityId', PlacesController.getCityDetail);
|
||||
|
||||
module.exports = router;
|
||||
15
backend/src/routes/prayerTimes.js
Normal file
15
backend/src/routes/prayerTimes.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// hDiyanetProxy - Prayer Times Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const PrayerTimesController = require('../controllers/prayerTimesController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// GET /api/v1/prayer-times?cityId={id}&startDate={date}&endDate={date}
|
||||
router.get('/', PrayerTimesController.getByDateRange);
|
||||
|
||||
// GET /api/v1/prayer-times/today?cityId={id}
|
||||
router.get('/today', PrayerTimesController.getToday);
|
||||
|
||||
module.exports = router;
|
||||
15
backend/src/routes/ramadan.js
Normal file
15
backend/src/routes/ramadan.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// hDiyanetProxy - Ramadan Routes
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const RamadanController = require('../controllers/ramadanController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// GET /api/v1/ramadan?cityId={id}
|
||||
router.get('/', RamadanController.getByCityId);
|
||||
|
||||
// GET /api/v1/ramadan/today?cityId={id}
|
||||
router.get('/today', RamadanController.getToday);
|
||||
|
||||
module.exports = router;
|
||||
220
backend/src/services/diyanetService.js
Normal file
220
backend/src/services/diyanetService.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// hDiyanetProxy - Diyanet API Servisi
|
||||
// Diyanet İşleri Başkanlığı API'sinden veri çekme işlemleri
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = process.env.DIYANET_API_URL || 'https://awqatsalah.diyanet.gov.tr';
|
||||
const DIYANET_EMAIL = process.env.DIYANET_EMAIL;
|
||||
const DIYANET_PASSWORD = process.env.DIYANET_PASSWORD;
|
||||
|
||||
// Token bilgileri bellekte tutulur
|
||||
let accessToken = null;
|
||||
let refreshToken = null;
|
||||
let tokenExpiry = null;
|
||||
|
||||
// Axios instance oluştur
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
// SSL sertifika sorunları için
|
||||
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
||||
});
|
||||
|
||||
// Her istekten önce token ekle
|
||||
api.interceptors.request.use(async (config) => {
|
||||
// Login isteğine token ekleme
|
||||
if (config.url && config.url.includes('/Auth/Login')) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Token yoksa veya süresi dolmuşsa yenile
|
||||
if (!accessToken || (tokenExpiry && Date.now() >= tokenExpiry)) {
|
||||
await DiyanetService.authenticate();
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 401 hatası alırsa token yenile ve tekrar dene
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
try {
|
||||
// Önce refresh token dene
|
||||
if (refreshToken) {
|
||||
await DiyanetService.refreshAccessToken();
|
||||
} else {
|
||||
await DiyanetService.authenticate();
|
||||
}
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return api(originalRequest);
|
||||
} catch (authError) {
|
||||
// Refresh da başarısız olursa, sıfırdan login ol
|
||||
await DiyanetService.authenticate();
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return api(originalRequest);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const DiyanetService = {
|
||||
// --- Kimlik Doğrulama ---
|
||||
|
||||
// Email ve şifre ile giriş yap, token al
|
||||
async authenticate() {
|
||||
try {
|
||||
const response = await axios.post(`${BASE_URL}/Auth/Login`, {
|
||||
email: DIYANET_EMAIL,
|
||||
password: DIYANET_PASSWORD
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
accessToken = response.data.data.accessToken;
|
||||
refreshToken = response.data.data.refreshToken;
|
||||
|
||||
// JWT'den expiry süresini çıkar (güvenli tarafta kalmak için 5 dk erken yenile)
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
|
||||
tokenExpiry = (payload.exp * 1000) - (5 * 60 * 1000);
|
||||
} catch {
|
||||
// 55 dakika sonra yenile (varsayılan)
|
||||
tokenExpiry = Date.now() + (55 * 60 * 1000);
|
||||
}
|
||||
|
||||
console.log('✅ Diyanet API token alındı');
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`Diyanet auth hatası: ${response.data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Diyanet API kimlik doğrulama hatası:', error.message);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh token ile yeni access token al
|
||||
async refreshAccessToken() {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/Auth/RefreshToken/${refreshToken}`, {
|
||||
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
accessToken = response.data.data.accessToken;
|
||||
refreshToken = response.data.data.refreshToken;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
|
||||
tokenExpiry = (payload.exp * 1000) - (5 * 60 * 1000);
|
||||
} catch {
|
||||
tokenExpiry = Date.now() + (55 * 60 * 1000);
|
||||
}
|
||||
|
||||
console.log('🔄 Diyanet API token yenilendi');
|
||||
return true;
|
||||
}
|
||||
throw new Error('Token yenileme başarısız');
|
||||
} catch (error) {
|
||||
console.error('❌ Token yenileme hatası:', error.message);
|
||||
// Refresh başarısızsa sıfırdan login ol
|
||||
accessToken = null;
|
||||
refreshToken = null;
|
||||
tokenExpiry = null;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// --- Yer Bilgileri ---
|
||||
|
||||
// Yardımcı: API yanıtından veriyi çıkar
|
||||
// Diyanet API {data: [...], success: true} formatında dönüyor
|
||||
_extractData(response) {
|
||||
const body = response.data;
|
||||
if (body && typeof body === 'object' && 'data' in body) {
|
||||
return body.data;
|
||||
}
|
||||
return body;
|
||||
},
|
||||
|
||||
// Tüm ülkeleri çek
|
||||
async getCountries() {
|
||||
const response = await api.get('/api/Place/Countries');
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// Ülkeye göre eyaletleri çek
|
||||
async getStates(countryId) {
|
||||
const response = await api.get(`/api/Place/States/${countryId}`);
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// Tüm eyaletleri çek
|
||||
async getAllStates() {
|
||||
const response = await api.get('/api/Place/States');
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// Eyalete göre şehirleri çek
|
||||
async getCities(stateId) {
|
||||
const response = await api.get(`/api/Place/Cities/${stateId}`);
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// Tüm şehirleri çek
|
||||
async getAllCities() {
|
||||
const response = await api.get('/api/Place/Cities');
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// Şehir detayı çek
|
||||
async getCityDetail(cityId) {
|
||||
const response = await api.get(`/api/Place/CityDetail/${cityId}`);
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// --- Namaz Vakitleri ---
|
||||
|
||||
// Yıllık namaz vakitlerini çek
|
||||
async getYearlyPrayerTimes(cityId, startDate, endDate) {
|
||||
const response = await api.post('/api/AwqatSalah/Yearly', {
|
||||
cityId: parseInt(cityId),
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// --- Ramazan İmsakiyesi ---
|
||||
async getRamadanTimes(cityId) {
|
||||
const response = await api.get(`/api/AwqatSalah/Ramadan/${cityId}`);
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// --- Bayram Vakitleri ---
|
||||
async getEidTimes(cityId) {
|
||||
const response = await api.get(`/api/AwqatSalah/Eid/${cityId}`);
|
||||
return this._extractData(response);
|
||||
},
|
||||
|
||||
// --- Cache Temizleme ---
|
||||
async clearCache() {
|
||||
const response = await api.get('/api/Cache/ClearCache');
|
||||
return this._extractData(response);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DiyanetService;
|
||||
191
backend/src/services/fetchService.js
Normal file
191
backend/src/services/fetchService.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// hDiyanetProxy - Veri Çekme ve Senkronizasyon Servisi
|
||||
const DiyanetService = require('./diyanetService');
|
||||
const PlaceModel = require('../models/Place');
|
||||
const PrayerTimeModel = require('../models/PrayerTime');
|
||||
const RamadanModel = require('../models/RamadanTime');
|
||||
const EidModel = require('../models/EidTime');
|
||||
const FetchLogModel = require('../models/FetchLog');
|
||||
|
||||
const FetchService = {
|
||||
// Tüm yer bilgilerini çek ve veritabanına kaydet
|
||||
async fetchAndSavePlaces(userId = null) {
|
||||
let totalRecords = 0;
|
||||
try {
|
||||
// 1. Ülkeleri çek
|
||||
console.log('📍 Ülkeler çekiliyor...');
|
||||
const countries = await DiyanetService.getCountries();
|
||||
if (Array.isArray(countries)) {
|
||||
for (const c of countries) {
|
||||
await PlaceModel.upsertCountry(c.id, c.name);
|
||||
}
|
||||
totalRecords += countries.length;
|
||||
console.log(` ✅ ${countries.length} ülke kaydedildi`);
|
||||
}
|
||||
|
||||
// 2. Eyaletleri çek (her ülke için)
|
||||
console.log('📍 Eyaletler/İller çekiliyor...');
|
||||
if (Array.isArray(countries)) {
|
||||
for (const c of countries) {
|
||||
try {
|
||||
const states = await DiyanetService.getStates(c.id);
|
||||
if (Array.isArray(states)) {
|
||||
for (const s of states) {
|
||||
await PlaceModel.upsertState(s.id, s.name, c.id);
|
||||
}
|
||||
totalRecords += states.length;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ ${c.name} eyaletleri alınamadı: ${e.message}`);
|
||||
}
|
||||
// Rate limiting - API'yi yormamak için küçük bekleme
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
console.log(` ✅ Eyaletler kaydedildi`);
|
||||
}
|
||||
|
||||
// 3. Şehirleri çek - tüm eyaletler için
|
||||
console.log('📍 Şehirler/İlçeler çekiliyor...');
|
||||
const pool = require('../utils/db');
|
||||
const [allStates] = await pool.execute('SELECT id, name FROM states');
|
||||
for (const s of allStates) {
|
||||
try {
|
||||
const cities = await DiyanetService.getCities(s.id);
|
||||
if (Array.isArray(cities)) {
|
||||
for (const ci of cities) {
|
||||
await PlaceModel.upsertCity(ci.id, ci.name, s.id, ci.latitude || null, ci.longitude || null);
|
||||
}
|
||||
totalRecords += cities.length;
|
||||
}
|
||||
} catch (e) {
|
||||
// Bazı eyaletlerin şehirleri olmayabilir
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
console.log(` ✅ Şehirler kaydedildi`);
|
||||
|
||||
// Log kaydı
|
||||
await FetchLogModel.create(userId, 'fetch_places', null, null, 'Tüm yer bilgileri güncellendi', 'success', totalRecords);
|
||||
return { success: true, message: 'Yer bilgileri güncellendi', totalRecords };
|
||||
} catch (err) {
|
||||
await FetchLogModel.create(userId, 'fetch_places', null, null, `Hata: ${err.message}`, 'error', 0);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Yıllık namaz vakitlerini çek ve kaydet
|
||||
async fetchAndSaveYearly(cityId, startDate, endDate, userId = null) {
|
||||
try {
|
||||
console.log(`🕌 Namaz vakitleri çekiliyor: cityId=${cityId}, ${startDate} - ${endDate}`);
|
||||
const data = await DiyanetService.getYearlyPrayerTimes(cityId, startDate, endDate);
|
||||
|
||||
if (!data || !Array.isArray(data)) {
|
||||
throw new Error('API\'den geçersiz veri döndü');
|
||||
}
|
||||
|
||||
// Verileri dönüştür ve kaydet
|
||||
const timesArray = data.map(item => ({
|
||||
date: item.gregorianDateLong ? item.gregorianDateLong.split('T')[0] : item.gregorianDate,
|
||||
fajr: item.fajr || item.imsak,
|
||||
sunrise: item.sunrise || item.gunes,
|
||||
dhuhr: item.dhuhr || item.ogle,
|
||||
asr: item.asr || item.ikindi,
|
||||
maghrib: item.maghrib || item.aksam,
|
||||
isha: item.isha || item.yatsi,
|
||||
qibla: item.qibla || null
|
||||
}));
|
||||
|
||||
await PrayerTimeModel.bulkUpsert(cityId, timesArray);
|
||||
|
||||
// Şehir adını al
|
||||
const cityDetail = await PlaceModel.getCityDetail(cityId);
|
||||
const cityName = cityDetail ? cityDetail.name : `City ${cityId}`;
|
||||
|
||||
await FetchLogModel.create(userId, 'fetch_yearly', cityId, cityName,
|
||||
`${startDate} - ${endDate} arası namaz vakitleri çekildi`, 'success', timesArray.length);
|
||||
|
||||
console.log(` ✅ ${timesArray.length} namaz vakti kaydedildi`);
|
||||
return { success: true, message: `${timesArray.length} namaz vakti kaydedildi`, count: timesArray.length };
|
||||
} catch (err) {
|
||||
await FetchLogModel.create(userId, 'fetch_yearly', cityId, null, `Hata: ${err.message}`, 'error', 0);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Ramazan vakitlerini çek ve kaydet
|
||||
async fetchAndSaveRamadan(cityId, userId = null) {
|
||||
try {
|
||||
console.log(`🌙 Ramazan vakitleri çekiliyor: cityId=${cityId}`);
|
||||
const data = await DiyanetService.getRamadanTimes(cityId);
|
||||
|
||||
if (!data || !Array.isArray(data)) {
|
||||
throw new Error('API\'den geçersiz veri döndü');
|
||||
}
|
||||
|
||||
const timesArray = data.map(item => ({
|
||||
date: item.gregorianDateLong ? item.gregorianDateLong.split('T')[0] : item.gregorianDate,
|
||||
fajr: item.fajr || item.imsak,
|
||||
sunrise: item.sunrise || item.gunes,
|
||||
dhuhr: item.dhuhr || item.ogle,
|
||||
asr: item.asr || item.ikindi,
|
||||
maghrib: item.maghrib || item.aksam,
|
||||
isha: item.isha || item.yatsi
|
||||
}));
|
||||
|
||||
await RamadanModel.bulkUpsert(cityId, timesArray);
|
||||
|
||||
const cityDetail = await PlaceModel.getCityDetail(cityId);
|
||||
const cityName = cityDetail ? cityDetail.name : `City ${cityId}`;
|
||||
|
||||
await FetchLogModel.create(userId, 'fetch_ramadan', cityId, cityName,
|
||||
'Ramazan vakitleri çekildi', 'success', timesArray.length);
|
||||
|
||||
console.log(` ✅ ${timesArray.length} ramazan vakti kaydedildi`);
|
||||
return { success: true, message: `${timesArray.length} ramazan vakti kaydedildi`, count: timesArray.length };
|
||||
} catch (err) {
|
||||
await FetchLogModel.create(userId, 'fetch_ramadan', cityId, null, `Hata: ${err.message}`, 'error', 0);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Bayram vakitlerini çek ve kaydet
|
||||
async fetchAndSaveEid(cityId, userId = null) {
|
||||
try {
|
||||
console.log(`🎉 Bayram vakitleri çekiliyor: cityId=${cityId}`);
|
||||
const data = await DiyanetService.getEidTimes(cityId);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('API\'den geçersiz veri döndü');
|
||||
}
|
||||
|
||||
// Bayram verisi tek obje veya dizi olabilir
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
const eidItems = items.map(item => ({
|
||||
eidName: item.eidName || item.bayramAdi || 'Bayram',
|
||||
date: item.gregorianDateLong ? item.gregorianDateLong.split('T')[0] : item.gregorianDate || item.date,
|
||||
prayerTime: item.eidPrayerTime || item.bayramNamazi || null,
|
||||
fajr: item.fajr || item.imsak || null,
|
||||
sunrise: item.sunrise || item.gunes || null,
|
||||
dhuhr: item.dhuhr || item.ogle || null,
|
||||
asr: item.asr || item.ikindi || null,
|
||||
maghrib: item.maghrib || item.aksam || null,
|
||||
isha: item.isha || item.yatsi || null
|
||||
}));
|
||||
|
||||
await EidModel.bulkUpsert(cityId, eidItems);
|
||||
|
||||
const cityDetail = await PlaceModel.getCityDetail(cityId);
|
||||
const cityName = cityDetail ? cityDetail.name : `City ${cityId}`;
|
||||
|
||||
await FetchLogModel.create(userId, 'fetch_eid', cityId, cityName,
|
||||
'Bayram vakitleri çekildi', 'success', eidItems.length);
|
||||
|
||||
console.log(` ✅ ${eidItems.length} bayram vakti kaydedildi`);
|
||||
return { success: true, message: `${eidItems.length} bayram vakti kaydedildi`, count: eidItems.length };
|
||||
} catch (err) {
|
||||
await FetchLogModel.create(userId, 'fetch_eid', cityId, null, `Hata: ${err.message}`, 'error', 0);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = FetchService;
|
||||
27
backend/src/utils/db.js
Normal file
27
backend/src/utils/db.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// hDiyanetProxy - Veritabanı bağlantı modülü
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
charset: 'utf8mb4'
|
||||
});
|
||||
|
||||
// Bağlantı testi
|
||||
pool.getConnection()
|
||||
.then(conn => {
|
||||
console.log('✅ MySQL veritabanına bağlandı');
|
||||
conn.release();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('❌ MySQL bağlantı hatası:', err.message);
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
134
backend/src/utils/migrate.js
Normal file
134
backend/src/utils/migrate.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// hDiyanetProxy - Migration Script
|
||||
// Veritabanı tablolarını oluşturur
|
||||
const pool = require('./db');
|
||||
|
||||
const migrations = [
|
||||
// Kullanıcılar tablosu
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'user') DEFAULT 'user',
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Ülkeler tablosu
|
||||
`CREATE TABLE IF NOT EXISTS countries (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Eyaletler/İller tablosu
|
||||
`CREATE TABLE IF NOT EXISTS states (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
country_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_country (country_id),
|
||||
FOREIGN KEY (country_id) REFERENCES countries(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Şehirler/İlçeler tablosu
|
||||
`CREATE TABLE IF NOT EXISTS cities (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
state_id INT NOT NULL,
|
||||
latitude DECIMAL(10,7) NULL,
|
||||
longitude DECIMAL(10,7) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_state (state_id),
|
||||
FOREIGN KEY (state_id) REFERENCES states(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Namaz vakitleri tablosu
|
||||
`CREATE TABLE IF NOT EXISTS prayer_times (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
city_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
fajr VARCHAR(10),
|
||||
sunrise VARCHAR(10),
|
||||
dhuhr VARCHAR(10),
|
||||
asr VARCHAR(10),
|
||||
maghrib VARCHAR(10),
|
||||
isha VARCHAR(10),
|
||||
qibla VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_city_date (city_id, date),
|
||||
INDEX idx_city (city_id),
|
||||
INDEX idx_date (date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Ramazan vakitleri tablosu
|
||||
`CREATE TABLE IF NOT EXISTS ramadan_times (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
city_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
fajr VARCHAR(10),
|
||||
sunrise VARCHAR(10),
|
||||
dhuhr VARCHAR(10),
|
||||
asr VARCHAR(10),
|
||||
maghrib VARCHAR(10),
|
||||
isha VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_city_date (city_id, date),
|
||||
INDEX idx_city (city_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Bayram vakitleri tablosu
|
||||
`CREATE TABLE IF NOT EXISTS eid_times (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
city_id INT NOT NULL,
|
||||
eid_name VARCHAR(100),
|
||||
date DATE NOT NULL,
|
||||
prayer_time VARCHAR(10),
|
||||
fajr VARCHAR(10),
|
||||
sunrise VARCHAR(10),
|
||||
dhuhr VARCHAR(10),
|
||||
asr VARCHAR(10),
|
||||
maghrib VARCHAR(10),
|
||||
isha VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_city_eid_date (city_id, eid_name, date),
|
||||
INDEX idx_city (city_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||
|
||||
// Veri çekme logları tablosu
|
||||
`CREATE TABLE IF NOT EXISTS fetch_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
city_id INT NULL,
|
||||
city_name VARCHAR(255) NULL,
|
||||
details TEXT NULL,
|
||||
status ENUM('success', 'error') DEFAULT 'success',
|
||||
records_count INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
||||
];
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('🔄 Migration başlatılıyor...\n');
|
||||
|
||||
for (let i = 0; i < migrations.length; i++) {
|
||||
const tableName = migrations[i].match(/CREATE TABLE IF NOT EXISTS (\w+)/)?.[1] || `migration_${i}`;
|
||||
try {
|
||||
await pool.execute(migrations[i]);
|
||||
console.log(` ✅ ${tableName} tablosu oluşturuldu`);
|
||||
} catch (err) {
|
||||
console.error(` ❌ ${tableName} hatası: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Migration tamamlandı!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
31
backend/src/utils/seed.js
Normal file
31
backend/src/utils/seed.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// hDiyanetProxy - Seed Script
|
||||
// İlk admin kullanıcıyı oluşturur
|
||||
const pool = require('./db');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
async function seed() {
|
||||
console.log('🌱 Seed başlatılıyor...\n');
|
||||
|
||||
try {
|
||||
// Admin kullanıcı kontrolü
|
||||
const [existing] = await pool.execute('SELECT id FROM users WHERE username = ?', ['admin']);
|
||||
|
||||
if (existing.length > 0) {
|
||||
console.log(' ℹ️ Admin kullanıcı zaten mevcut, atlanıyor');
|
||||
} else {
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
await pool.execute(
|
||||
'INSERT INTO users (username, password, role, is_active) VALUES (?, ?, ?, ?)',
|
||||
['admin', hashedPassword, 'admin', 1]
|
||||
);
|
||||
console.log(' ✅ Admin kullanıcı oluşturuldu (admin / admin123)');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(' ❌ Seed hatası:', err.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ Seed tamamlandı!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
seed();
|
||||
75
frontend/src/components/sidebar.js
Normal file
75
frontend/src/components/sidebar.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// hDiyanetProxy - Sidebar Bileşeni
|
||||
function renderSidebar(activePage) {
|
||||
const user = getUser();
|
||||
const isAdmin = user && user.role === 'admin';
|
||||
|
||||
return `
|
||||
<button class="mobile-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>🕌 hDiyanetProxy</h2>
|
||||
<small>Namaz Vakitleri Proxy API</small>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">Genel</div>
|
||||
<a href="/dashboard" class="${activePage === 'dashboard' ? 'active' : ''}">📊 Dashboard</a>
|
||||
<a href="/prayer-times" class="${activePage === 'prayer-times' ? 'active' : ''}">🕐 Namaz Vakitleri</a>
|
||||
<a href="/ramadan" class="${activePage === 'ramadan' ? 'active' : ''}">🌙 Ramazan / İftar</a>
|
||||
${isAdmin ? `
|
||||
<div class="nav-section">Yönetim</div>
|
||||
<a href="/fetch" class="${activePage === 'fetch' ? 'active' : ''}">📥 Veri Çekme</a>
|
||||
<a href="/admin/users" class="${activePage === 'admin-users' ? 'active' : ''}">👥 Kullanıcılar</a>
|
||||
` : ''}
|
||||
<div class="nav-section">Hesap</div>
|
||||
<a href="/profile" class="${activePage === 'profile' ? 'active' : ''}">👤 Profil & API</a>
|
||||
<a href="#" onclick="logout(); return false;">🚪 Çıkış Yap</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<span>👤 ${user ? user.username : 'Misafir'}</span>
|
||||
${user ? `<span class="user-badge">${user.role}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
// Ülke-Eyalet-Şehir dropdown zinciri yükleme
|
||||
async function loadCountryDropdown(selectId) {
|
||||
try {
|
||||
const countries = await apiFetch('/places/countries');
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">Ülke seçin...</option>';
|
||||
countries.forEach(c => {
|
||||
select.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Ülkeler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStateDropdown(countryId, selectId) {
|
||||
try {
|
||||
const states = await apiFetch(`/places/states?countryId=${countryId}`);
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">İl/Eyalet seçin...</option>';
|
||||
states.forEach(s => {
|
||||
select.innerHTML += `<option value="${s.id}">${s.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Eyaletler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCityDropdown(stateId, selectId) {
|
||||
try {
|
||||
const cities = await apiFetch(`/places/cities?stateId=${stateId}`);
|
||||
const select = document.getElementById(selectId);
|
||||
select.innerHTML = '<option value="">Şehir/İlçe seçin...</option>';
|
||||
cities.forEach(c => {
|
||||
select.innerHTML += `<option value="${c.id}">${c.name}</option>`;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Şehirler yüklenemedi:', err);
|
||||
}
|
||||
}
|
||||
157
frontend/src/pages/admin-users.html
Normal file
157
frontend/src/pages/admin-users.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Kullanıcı Yönetimi - hDiyanetProxy</title>
|
||||
<meta name="description" content="Admin kullanıcı yönetim paneli">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('admin-users') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>👥 Kullanıcı Yönetimi</h1>
|
||||
<p>Kullanıcıları listeleyin, ekleyin ve yönetin</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Yeni Kullanıcı Ekle</h3>
|
||||
</div>
|
||||
<form onsubmit="createUser(event)" style="display:flex; gap:12px; flex-wrap:wrap; align-items:end;">
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newUsername">Kullanıcı Adı</label>
|
||||
<input type="text" id="newUsername" class="form-control" required minlength="3" maxlength="50" placeholder="kullanici_adi">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newPassword">Şifre</label>
|
||||
<input type="password" id="newPassword" class="form-control" required minlength="6" placeholder="••••••">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label for="newRole">Rol</label>
|
||||
<select id="newRole" class="form-control">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="btnCreate">➕ Ekle</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Kullanıcı Listesi</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Kullanıcı Adı</th>
|
||||
<th>Rol</th>
|
||||
<th>Durum</th>
|
||||
<th>Oluşturulma</th>
|
||||
<th>İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersBody">
|
||||
<tr><td colspan="6" style="text-align:center; padding:20px;">Yükleniyor...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadUsers();
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const users = await apiFetch('/admin/users');
|
||||
const body = document.getElementById('usersBody');
|
||||
const currentUser = getUser();
|
||||
|
||||
body.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td><strong>${u.username}</strong></td>
|
||||
<td><span class="badge badge-${u.role === 'admin' ? 'admin' : 'user'}">${u.role}</span></td>
|
||||
<td><span class="badge badge-${u.is_active ? 'success' : 'error'}">${u.is_active ? 'Aktif' : 'Pasif'}</span></td>
|
||||
<td>${formatDateTime(u.created_at)}</td>
|
||||
<td>
|
||||
${u.id !== currentUser.id ? `
|
||||
<button class="btn btn-outline btn-sm" onclick="toggleUser(${u.id}, ${!u.is_active})">
|
||||
${u.is_active ? '⏸ Pasif Yap' : '▶️ Aktif Yap'}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id}, '${u.username}')">🗑️</button>
|
||||
` : '<span style="color:var(--text-lighter); font-size:12px;">Mevcut Kullanıcı</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnCreate');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await apiFetch('/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('newUsername').value,
|
||||
password: document.getElementById('newPassword').value,
|
||||
role: document.getElementById('newRole').value
|
||||
})
|
||||
});
|
||||
showToast('Kullanıcı oluşturuldu!', 'success');
|
||||
document.getElementById('newUsername').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUser(id, isActive) {
|
||||
try {
|
||||
await apiFetch(`/admin/users/${id}/toggle`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isActive })
|
||||
});
|
||||
showToast(isActive ? 'Kullanıcı aktif edildi' : 'Kullanıcı pasif edildi', 'success');
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id, username) {
|
||||
if (!confirm(`"${username}" kullanıcısını silmek istediğinize emin misiniz?`)) return;
|
||||
|
||||
try {
|
||||
await apiFetch(`/admin/users/${id}`, { method: 'DELETE' });
|
||||
showToast('Kullanıcı silindi', 'success');
|
||||
loadUsers();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
frontend/src/pages/dashboard.html
Normal file
97
frontend/src/pages/dashboard.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - hDiyanetProxy</title>
|
||||
<meta name="description" content="hDiyanetProxy Dashboard - Sistem genel durumu ve istatistikler">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('dashboard') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>📊 Dashboard</h1>
|
||||
<p>Sistem genel durumu ve istatistikler</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card"><div class="stat-icon">⏳</div><div class="stat-value">-</div><div class="stat-label">Yükleniyor...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Son Veri Çekme Logları</h3>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>Kullanıcı</th>
|
||||
<th>İşlem</th>
|
||||
<th>Şehir</th>
|
||||
<th>Durum</th>
|
||||
<th>Kayıt</th>
|
||||
<th>Detay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
<tr><td colspan="7" style="text-align:center; padding:20px;">Yükleniyor...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadDashboard();
|
||||
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const data = await apiFetch('/admin/dashboard');
|
||||
|
||||
// İstatistikler
|
||||
const s = data.stats;
|
||||
document.getElementById('statsGrid').innerHTML = `
|
||||
<div class="stat-card"><div class="stat-icon">🌍</div><div class="stat-value">${s.countries}</div><div class="stat-label">Ülke</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🏙️</div><div class="stat-value">${s.states}</div><div class="stat-label">İl/Eyalet</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">📍</div><div class="stat-value">${s.cities}</div><div class="stat-label">Şehir/İlçe</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🕐</div><div class="stat-value">${s.prayerTimes}</div><div class="stat-label">Namaz Vakti</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🌙</div><div class="stat-value">${s.ramadanTimes}</div><div class="stat-label">Ramazan Kaydı</div></div>
|
||||
<div class="stat-card"><div class="stat-icon">🎉</div><div class="stat-value">${s.eidTimes}</div><div class="stat-label">Bayram Kaydı</div></div>
|
||||
`;
|
||||
|
||||
// Loglar
|
||||
const logsBody = document.getElementById('logsBody');
|
||||
if (data.recentLogs.length === 0) {
|
||||
logsBody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:20px; color:var(--text-light);">Henüz log kaydı yok</td></tr>';
|
||||
} else {
|
||||
logsBody.innerHTML = data.recentLogs.map(log => `
|
||||
<tr>
|
||||
<td>${formatDateTime(log.created_at)}</td>
|
||||
<td>${log.username || 'Sistem'}</td>
|
||||
<td><span class="badge badge-info">${log.action}</span></td>
|
||||
<td>${log.city_name || '-'}</td>
|
||||
<td><span class="badge badge-${log.status === 'success' ? 'success' : 'error'}">${log.status}</span></td>
|
||||
<td>${log.records_count}</td>
|
||||
<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${log.details || ''}">${log.details || '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
237
frontend/src/pages/fetch.html
Normal file
237
frontend/src/pages/fetch.html
Normal file
@@ -0,0 +1,237 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Veri Çekme - hDiyanetProxy</title>
|
||||
<meta name="description" content="Diyanet API'den veri çekme paneli">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
app.innerHTML = renderSidebar('fetch') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>📥 Veri Çekme Paneli</h1>
|
||||
<p>Diyanet API'den veri çekip veritabanına kaydedin</p>
|
||||
</div>
|
||||
|
||||
<!-- Yer Bilgileri Çekme -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📍 Yer Bilgileri</h3>
|
||||
<button class="btn btn-primary btn-sm" onclick="fetchPlaces()" id="btnFetchPlaces">
|
||||
📥 Tüm Yer Bilgilerini Çek
|
||||
</button>
|
||||
</div>
|
||||
<p style="color:var(--text-light); font-size:13px;">Ülke, eyalet ve şehir bilgilerini Diyanet API'den çeker. İlk kurulumda ve periyodik olarak çalıştırılmalıdır.</p>
|
||||
</div>
|
||||
|
||||
<!-- Şehir Seçimi -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🏙️ Şehir Seçimi</h3>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Veri Çekme İşlemleri -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🕌 Veri Çekme İşlemleri</h3>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:16px;">
|
||||
<div class="form-group">
|
||||
<label for="startDate">Başlangıç Tarihi</label>
|
||||
<input type="date" id="startDate" class="form-control" value="${currentYear}-01-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endDate">Bitiş Tarihi</label>
|
||||
<input type="date" id="endDate" class="form-control" value="${currentYear}-12-31">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<button class="btn btn-primary" onclick="fetchYearly()" id="btnYearly">🕐 Yıllık Namaz Vakitlerini Çek</button>
|
||||
<button class="btn btn-accent" onclick="fetchRamadan()" id="btnRamadan">🌙 Ramazan Verilerini Çek</button>
|
||||
<button class="btn btn-primary" onclick="fetchEid()" id="btnEid">🎉 Bayram Verilerini Çek</button>
|
||||
<button class="btn btn-outline" onclick="clearDiyanetCache()" id="btnCache">🗑️ Diyanet Cache Temizle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- İşlem Konsolu -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📟 İşlem Konsolu</h3>
|
||||
<button class="btn btn-outline btn-sm" onclick="document.getElementById('logConsole').textContent = '> Konsol temizlendi\\n'">Temizle</button>
|
||||
</div>
|
||||
<div class="log-console" id="logConsole">> Hazır. Bir işlem seçin ve başlatın.\n</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
// Sayfa yüklendiğinde ülkeleri yükle
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
function addLog(msg, type = 'info') {
|
||||
const el = document.getElementById('logConsole');
|
||||
const span = document.createElement('span');
|
||||
span.className = `log-${type}`;
|
||||
span.textContent = `> ${msg}\n`;
|
||||
el.appendChild(span);
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function getSelectedCityId() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) {
|
||||
showToast('Lütfen bir şehir seçin', 'warning');
|
||||
return null;
|
||||
}
|
||||
return cityId;
|
||||
}
|
||||
|
||||
async function fetchPlaces() {
|
||||
const btn = document.getElementById('btnFetchPlaces');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog('Yer bilgileri çekiliyor... (Bu işlem birkaç dakika sürebilir)', 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/places', { method: 'POST' });
|
||||
addLog(`✅ ${result.message} (${result.totalRecords} kayıt)`, 'success');
|
||||
showToast('Yer bilgileri güncellendi!', 'success');
|
||||
loadCountryDropdown('countrySelect');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '📥 Tüm Yer Bilgilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchYearly() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const startDate = document.getElementById('startDate').value + 'T00:00:00.0Z';
|
||||
const endDate = document.getElementById('endDate').value + 'T23:59:59.0Z';
|
||||
const btn = document.getElementById('btnYearly');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Namaz vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/yearly', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId), startDate, endDate })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🕐 Yıllık Namaz Vakitlerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRamadan() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const btn = document.getElementById('btnRamadan');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Ramazan vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/ramadan', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId) })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🌙 Ramazan Verilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEid() {
|
||||
const cityId = getSelectedCityId();
|
||||
if (!cityId) return;
|
||||
|
||||
const btn = document.getElementById('btnEid');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Çekiliyor...';
|
||||
addLog(`Bayram vakitleri çekiliyor: cityId=${cityId}`, 'info');
|
||||
|
||||
try {
|
||||
const result = await apiFetch('/admin/fetch/eid', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cityId: parseInt(cityId) })
|
||||
});
|
||||
addLog(`✅ ${result.message}`, 'success');
|
||||
showToast(result.message, 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🎉 Bayram Verilerini Çek';
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDiyanetCache() {
|
||||
const btn = document.getElementById('btnCache');
|
||||
btn.disabled = true;
|
||||
addLog('Diyanet API cache temizleniyor...', 'info');
|
||||
|
||||
try {
|
||||
await apiFetch('/admin/cache/clear', { method: 'POST' });
|
||||
addLog('✅ Cache temizlendi', 'success');
|
||||
showToast('Cache temizlendi', 'success');
|
||||
} catch (err) {
|
||||
addLog(`❌ Hata: ${err.message}`, 'error');
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
71
frontend/src/pages/login.html
Normal file
71
frontend/src/pages/login.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Giriş - hDiyanetProxy</title>
|
||||
<meta name="description" content="hDiyanetProxy - Namaz Vakitleri Proxy API Giriş">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h1>🕌 hDiyanetProxy</h1>
|
||||
<p class="subtitle">Namaz Vakitleri Proxy API</p>
|
||||
|
||||
<div id="loginError" class="login-error"></div>
|
||||
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">Kullanıcı Adı</label>
|
||||
<input type="text" id="username" class="form-control" placeholder="admin" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Şifre</label>
|
||||
<input type="password" id="password" class="form-control" placeholder="••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">Giriş Yap</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script>
|
||||
// Zaten giriş yapılmışsa dashboard'a yönlendir
|
||||
if (getToken()) {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('loginBtn');
|
||||
const errorEl = document.getElementById('loginError');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Giriş yapılıyor...';
|
||||
errorEl.style.display = 'none';
|
||||
|
||||
try {
|
||||
const data = await apiFetch('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
window.location.href = '/dashboard';
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.style.display = 'block';
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Giriş Yap';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
157
frontend/src/pages/prayer-times.html
Normal file
157
frontend/src/pages/prayer-times.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Namaz Vakitleri - hDiyanetProxy</title>
|
||||
<meta name="description" content="Namaz vakitleri sorgulama ve listeleme">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
let currentData = [];
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('prayer-times') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>🕐 Namaz Vakitleri</h1>
|
||||
<p>Şehir ve tarih aralığı seçerek namaz vakitlerini sorgulayın</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:16px;">
|
||||
<div class="form-group">
|
||||
<label for="startDate">Başlangıç Tarihi</label>
|
||||
<input type="date" id="startDate" class="form-control" value="${today}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endDate">Bitiş Tarihi</label>
|
||||
<input type="date" id="endDate" class="form-control" value="${today}">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn btn-primary" onclick="queryPrayerTimes()" id="btnQuery">🔍 Sorgula</button>
|
||||
<button class="btn btn-accent" onclick="queryToday()" id="btnToday">📅 Bugün</button>
|
||||
<div class="export-btns" style="margin-left:auto;">
|
||||
<button class="btn btn-outline btn-sm" onclick="exportCSV(currentData, 'namaz_vakitleri')">📄 CSV</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="exportJSON(currentData, 'namaz_vakitleri')">📋 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>İmsak</th>
|
||||
<th>Güneş</th>
|
||||
<th>Öğle</th>
|
||||
<th>İkindi</th>
|
||||
<th>Akşam</th>
|
||||
<th>Yatsı</th>
|
||||
<th>Kıble</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timesBody">
|
||||
<tr><td colspan="8" style="text-align:center; padding:40px; color:var(--text-light);">Bir şehir seçip sorgulayın</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
async function queryPrayerTimes() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
const btn = document.getElementById('btnQuery');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/prayer-times?cityId=${cityId}&startDate=${startDate}&endDate=${endDate}`);
|
||||
currentData = data;
|
||||
renderTable(data);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔍 Sorgula';
|
||||
}
|
||||
}
|
||||
|
||||
async function queryToday() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/prayer-times/today?cityId=${cityId}`);
|
||||
currentData = data ? [data] : [];
|
||||
renderTable(currentData);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const body = document.getElementById('timesBody');
|
||||
if (!data || data.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="8" style="text-align:center; padding:40px; color:var(--text-light);">Kayıt bulunamadı</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const todayStr = today;
|
||||
body.innerHTML = data.map(row => {
|
||||
const rowDate = row.date ? row.date.split('T')[0] : '';
|
||||
const isToday = rowDate === todayStr;
|
||||
return `<tr style="${isToday ? 'background:rgba(26,107,78,0.06); font-weight:600;' : ''}">
|
||||
<td>${formatDate(row.date)}</td>
|
||||
<td>${row.fajr || '-'}</td>
|
||||
<td>${row.sunrise || '-'}</td>
|
||||
<td>${row.dhuhr || '-'}</td>
|
||||
<td>${row.asr || '-'}</td>
|
||||
<td>${row.maghrib || '-'}</td>
|
||||
<td>${row.isha || '-'}</td>
|
||||
<td>${row.qibla || '-'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
155
frontend/src/pages/profile.html
Normal file
155
frontend/src/pages/profile.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Profil & API - hDiyanetProxy</title>
|
||||
<meta name="description" content="Profil bilgileri ve API token yönetimi">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
const user = getUser();
|
||||
const token = getToken();
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('profile') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>👤 Profil & API Token</h1>
|
||||
<p>Hesap bilgileriniz ve API erişim token'ınız</p>
|
||||
</div>
|
||||
|
||||
<!-- Profil Bilgileri -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Hesap Bilgileri</h3>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns:150px 1fr; gap:12px; font-size:14px;">
|
||||
<strong>Kullanıcı Adı:</strong> <span>${user ? user.username : '-'}</span>
|
||||
<strong>Rol:</strong> <span class="badge badge-${user?.role === 'admin' ? 'admin' : 'user'}">${user ? user.role : '-'}</span>
|
||||
<strong>ID:</strong> <span>${user ? user.id : '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Token -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔑 API Token</h3>
|
||||
<button class="btn btn-primary btn-sm" onclick="refreshToken()">🔄 Yeni Token Üret</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mevcut Token (Bearer)</label>
|
||||
<textarea id="tokenDisplay" class="form-control" rows="3" readonly style="font-family:monospace; font-size:12px; resize:none;">${token || ''}</textarea>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="copyToken()">📋 Kopyala</button>
|
||||
</div>
|
||||
|
||||
<!-- API Kullanım -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📖 API Kullanımı</h3>
|
||||
</div>
|
||||
<div style="font-size:13px; color:var(--text-light); line-height:1.8;">
|
||||
<p>Tüm API isteklerinde <code>Authorization: Bearer {token}</code> header'ı gereklidir.</p>
|
||||
<br>
|
||||
<strong>Örnek istek:</strong>
|
||||
<div class="log-console" style="margin-top:8px; font-size:12px;">
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \\
|
||||
http://localhost:3000/api/v1/prayer-times/today?cityId=9541
|
||||
|
||||
# Tüm Endpointler:
|
||||
# POST /api/v1/auth/login
|
||||
# POST /api/v1/auth/register
|
||||
# GET /api/v1/places/countries
|
||||
# GET /api/v1/places/states?countryId={id}
|
||||
# GET /api/v1/places/cities?stateId={id}
|
||||
# GET /api/v1/places/city/{cityId}
|
||||
# GET /api/v1/prayer-times?cityId={id}&startDate={date}&endDate={date}
|
||||
# GET /api/v1/prayer-times/today?cityId={id}
|
||||
# GET /api/v1/ramadan?cityId={id}
|
||||
# GET /api/v1/ramadan/today?cityId={id}
|
||||
# GET /api/v1/eid?cityId={id}
|
||||
# POST /api/v1/admin/fetch/places
|
||||
# POST /api/v1/admin/fetch/yearly
|
||||
# POST /api/v1/admin/fetch/ramadan
|
||||
# POST /api/v1/admin/fetch/eid
|
||||
# POST /api/v1/admin/cache/clear
|
||||
# GET /api/v1/admin/logs
|
||||
# GET /api/v1/admin/users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Şifre Değiştir -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>🔒 Şifre Değiştir</h3>
|
||||
</div>
|
||||
<form onsubmit="changePassword(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Mevcut Şifre</label>
|
||||
<input type="password" id="currentPassword" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">Yeni Şifre</label>
|
||||
<input type="password" id="newPassword" class="form-control" required minlength="6">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="btnChangePass">🔒 Şifreyi Güncelle</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
async function refreshToken() {
|
||||
try {
|
||||
const data = await apiFetch('/auth/refresh', { method: 'POST' });
|
||||
setToken(data.token);
|
||||
document.getElementById('tokenDisplay').value = data.token;
|
||||
showToast('Yeni token üretildi!', 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function copyToken() {
|
||||
const el = document.getElementById('tokenDisplay');
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
showToast('Token panoya kopyalandı', 'info');
|
||||
}
|
||||
|
||||
async function changePassword(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnChangePass');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await apiFetch('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
currentPassword: document.getElementById('currentPassword').value,
|
||||
newPassword: document.getElementById('newPassword').value
|
||||
})
|
||||
});
|
||||
showToast('Şifre başarıyla güncellendi!', 'success');
|
||||
document.getElementById('currentPassword').value = '';
|
||||
document.getElementById('newPassword').value = '';
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
141
frontend/src/pages/ramadan.html
Normal file
141
frontend/src/pages/ramadan.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ramazan / İftar - hDiyanetProxy</title>
|
||||
<meta name="description" content="Ramazan imsakiyesi ve iftar vakitleri">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout" id="app"></div>
|
||||
|
||||
<script src="/services/api.js"></script>
|
||||
<script src="/components/sidebar.js"></script>
|
||||
<script>
|
||||
if (!requireAuth()) throw new Error('Auth');
|
||||
|
||||
let currentData = [];
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = renderSidebar('ramadan') + `
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<h1>🌙 Ramazan / İftar Vakitleri</h1>
|
||||
<p>Ramazan imsakiyesi ve iftar vakitlerini sorgulayın</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="countrySelect">Ülke</label>
|
||||
<select id="countrySelect" class="form-control" onchange="loadStateDropdown(this.value, 'stateSelect'); document.getElementById('citySelect').innerHTML='<option value=\\'\\'>Önce il seçin...</option>';">
|
||||
<option value="">Yükleniyor...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stateSelect">İl / Eyalet</label>
|
||||
<select id="stateSelect" class="form-control" onchange="loadCityDropdown(this.value, 'citySelect')">
|
||||
<option value="">Önce ülke seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="citySelect">Şehir / İlçe</label>
|
||||
<select id="citySelect" class="form-control">
|
||||
<option value="">Önce il seçin...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn btn-accent" onclick="queryRamadan()" id="btnQuery">🌙 Ramazan Takvimini Getir</button>
|
||||
<button class="btn btn-primary" onclick="queryRamadanToday()" id="btnToday">📅 Bugün</button>
|
||||
<div class="export-btns" style="margin-left:auto;">
|
||||
<button class="btn btn-outline btn-sm" onclick="exportCSV(currentData, 'ramazan_vakitleri')">📄 CSV</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="exportJSON(currentData, 'ramazan_vakitleri')">📋 JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tarih</th>
|
||||
<th>İmsak (Sahur)</th>
|
||||
<th>Güneş</th>
|
||||
<th>Öğle</th>
|
||||
<th>İkindi</th>
|
||||
<th>Akşam (İftar)</th>
|
||||
<th>Yatsı</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timesBody">
|
||||
<tr><td colspan="7" style="text-align:center; padding:40px; color:var(--text-light);">Bir şehir seçip sorgulayın</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
loadCountryDropdown('countrySelect');
|
||||
|
||||
async function queryRamadan() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
const btn = document.getElementById('btnQuery');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/ramadan?cityId=${cityId}`);
|
||||
currentData = data;
|
||||
renderRamadanTable(data);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🌙 Ramazan Takvimini Getir';
|
||||
}
|
||||
}
|
||||
|
||||
async function queryRamadanToday() {
|
||||
const cityId = document.getElementById('citySelect').value;
|
||||
if (!cityId) { showToast('Şehir seçin', 'warning'); return; }
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/ramadan/today?cityId=${cityId}`);
|
||||
currentData = data ? [data] : [];
|
||||
renderRamadanTable(currentData);
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderRamadanTable(data) {
|
||||
const body = document.getElementById('timesBody');
|
||||
if (!data || data.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:40px; color:var(--text-light);">Kayıt bulunamadı</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
body.innerHTML = data.map(row => {
|
||||
const rowDate = row.date ? row.date.split('T')[0] : '';
|
||||
const isToday = rowDate === todayStr;
|
||||
return `<tr class="${isToday ? 'iftar-row' : ''}" style="${isToday ? 'background:rgba(232,168,56,0.1);' : ''}">
|
||||
<td>${formatDate(row.date)} ${isToday ? '<span class="badge badge-warning">BUGÜN</span>' : ''}</td>
|
||||
<td>${row.fajr || '-'}</td>
|
||||
<td>${row.sunrise || '-'}</td>
|
||||
<td>${row.dhuhr || '-'}</td>
|
||||
<td>${row.asr || '-'}</td>
|
||||
<td style="${isToday ? 'color:var(--accent-dark); font-weight:700; font-size:15px;' : ''}">${row.maghrib || '-'} ${isToday ? '🌙' : ''}</td>
|
||||
<td>${row.isha || '-'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
127
frontend/src/services/api.js
Normal file
127
frontend/src/services/api.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// hDiyanetProxy - Frontend API Servisi
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
// Token yönetimi
|
||||
function getToken() {
|
||||
return localStorage.getItem('hdiyanet_token');
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
localStorage.setItem('hdiyanet_token', token);
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
const user = localStorage.getItem('hdiyanet_user');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
function setUser(user) {
|
||||
localStorage.setItem('hdiyanet_user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('hdiyanet_token');
|
||||
localStorage.removeItem('hdiyanet_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Auth kontrolü - token yoksa login'e yönlendir
|
||||
function requireAuth() {
|
||||
if (!getToken()) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API çağrısı yardımcı fonksiyonu
|
||||
async function apiFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.headers
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
|
||||
// 401 ise login'e yönlendir
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('Oturum süresi doldu');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.errors?.[0]?.msg || 'Bir hata oluştu');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Toast bildirimi göster
|
||||
function showToast(message, type = 'info') {
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Tarih formatlama
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('tr-TR');
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString('tr-TR');
|
||||
}
|
||||
|
||||
// CSV dışa aktarma
|
||||
function exportCSV(data, filename) {
|
||||
if (!data || data.length === 0) return;
|
||||
const headers = Object.keys(data[0]);
|
||||
const csv = [
|
||||
headers.join(','),
|
||||
...data.map(row => headers.map(h => `"${row[h] || ''}"`).join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// JSON dışa aktarma
|
||||
function exportJSON(data, filename) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
567
frontend/src/style.css
Normal file
567
frontend/src/style.css
Normal file
@@ -0,0 +1,567 @@
|
||||
/* hDiyanetProxy - Ana Stil Dosyası */
|
||||
:root {
|
||||
--primary: #1a6b4e;
|
||||
--primary-light: #2d9d6f;
|
||||
--primary-dark: #0f4a35;
|
||||
--accent: #e8a838;
|
||||
--accent-dark: #c48a1f;
|
||||
--bg: #f5f7fa;
|
||||
--bg-card: #ffffff;
|
||||
--text: #1e293b;
|
||||
--text-light: #64748b;
|
||||
--text-lighter: #94a3b8;
|
||||
--border: #e2e8f0;
|
||||
--success: #16a34a;
|
||||
--error: #dc2626;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.08), 0 2px 4px -1px rgba(0,0,0,0.04);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -2px rgba(0,0,0,0.04);
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
a:hover { color: var(--primary-light); }
|
||||
|
||||
/* --- Layout --- */
|
||||
.app-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--primary-dark);
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-header small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.5);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.sidebar-nav a.active {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.12);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
|
||||
.sidebar-nav .nav-section {
|
||||
padding: 16px 20px 6px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: rgba(255,255,255,0.35);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sidebar-footer .user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.sidebar-footer .user-badge {
|
||||
background: var(--accent);
|
||||
color: var(--primary-dark);
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
padding: 24px 32px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --- Cards --- */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* --- Stats Grid --- */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-light);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-light); }
|
||||
|
||||
.btn-accent {
|
||||
background: var(--accent);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.btn-accent:hover { background: var(--accent-dark); color: #fff; }
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger:hover { background: #b91c1c; }
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Forms --- */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(26,107,78,0.1);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* --- Tables --- */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
thead th {
|
||||
background: var(--bg);
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-light);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: rgba(26,107,78,0.03);
|
||||
}
|
||||
|
||||
/* --- Badges --- */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge-success { background: #dcfce7; color: #166534; }
|
||||
.badge-error { background: #fef2f2; color: #991b1b; }
|
||||
.badge-info { background: #dbeafe; color: #1e40af; }
|
||||
.badge-warning { background: #fef9c3; color: #854d0e; }
|
||||
.badge-admin { background: var(--accent); color: var(--primary-dark); }
|
||||
.badge-user { background: #e2e8f0; color: #475569; }
|
||||
|
||||
/* --- Toast / Alert --- */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
padding: 12px 20px;
|
||||
border-radius: var(--radius);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.toast-success { background: var(--success); }
|
||||
.toast-error { background: var(--error); }
|
||||
.toast-info { background: var(--info); }
|
||||
.toast-warning { background: var(--warning); }
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* --- Login Page --- */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 50%, var(--primary-light) 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.login-card .subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-card .btn-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
background: #fef2f2;
|
||||
color: var(--error);
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Log Console --- */
|
||||
.log-console {
|
||||
background: #1e293b;
|
||||
color: #a5f3fc;
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.log-console .log-success { color: #4ade80; }
|
||||
.log-console .log-error { color: #f87171; }
|
||||
.log-console .log-info { color: #93c5fd; }
|
||||
|
||||
/* --- Loading --- */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255,255,255,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* --- Modal --- */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 480px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
.mobile-toggle {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 200;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Iftar vurgulu satır */
|
||||
tr.iftar-row {
|
||||
background: rgba(232,168,56,0.08) !important;
|
||||
}
|
||||
|
||||
tr.iftar-row td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Export butonları */
|
||||
.export-btns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
158
package-lock.json
generated
Normal file
158
package-lock.json
generated
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"name": "hdiyanetproxy",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hdiyanetproxy",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mysql2": "^3.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
|
||||
"integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.18.2",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz",
|
||||
"integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sql-escaper": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
|
||||
"integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0",
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "hdiyanetproxy",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mysql2": "^3.18.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user