diff --git a/.dockerignore b/.dockerignore index c3b48fc..232376b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ node_modules .next out +dist coverage *.tsbuildinfo next-env.d.ts diff --git a/.gitignore b/.gitignore index 56ea362..5eca11d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ next-env.d.ts # local tooling /scratch/ +/dist diff --git a/.prettierignore b/.prettierignore index f37c117..218f411 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ coverage uploads next-env.d.ts docs/PROMPT.md +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 91acb4e..cc5f1f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ Biçim [Keep a Changelog](https://keepachangelog.com/tr-TR/1.1.0/) esas alınır ## [Yayınlanmamış] +## [1.0.0] - 2026-11-07 + +Faz 15: canlıya alma (Dokploy + Gitea, `butcem.ta1sph.website`). + +### Eklendi + +- Konteyner başlatma adımı (`docker-entrypoint.sh` → `scripts/bootstrap.mjs`): MongoDB replica set ve PRIMARY kontrolü (yoksa açık hata mesajıyla durur), bekleyen migration'lar, `SEED_ADMIN_*` tanımlıysa ilk admin hesabı (varsa dokunulmaz). esbuild ile tek dosyaya derlenir; imajda `tsx` gerekmez. Acil durumda `SKIP_BOOTSTRAP=1`. +- `/api/health` yanıtında `replicaSet` alanı (transaction desteğinin canlıda doğrulanması için). +- `docs/DEPLOY.md`: Dokploy uygulama kurulumu, ortam değişkenleri, volume, domain/HTTPS, Gitea webhook ile otomatik deploy, güncelleme/geri alma, yedekleme, geri yükleme ve sorun giderme; her komut açıklamalı. +- `docs/MONGODB-KURULUM.md`: mongo:7 servisinde replica set kontrolü, keyfile ile tek düğümlü replica set'e geçiş, uygulama kullanıcısı yetkileri, bağlantı dizesi, transaction denemesi. +- `scripts/smoke-test.sh`: sağlık (veritabanı, replica set, sürüm), güvenlik başlıkları, HTTP→HTTPS, oturumsuz erişim engeli ve isteğe bağlı giriş kontrolü; veri değiştirmez. +- `scripts/backup/`: günlük yedek betiği (mongodump + uploads arşivi, SHA-256 özetleri, 7 gün saklama, parola komut satırında görünmez), onaylı geri yükleme betiği ve ayar örneği. + +### Değişti + +- Docker imajı: `ENTRYPOINT` başlatma adımıyla; sağlık kontrolü başlangıç payı 60 sn. +- Uygulama mongo:7.0 ile uyumlu (tüm entegrasyon testleri mongo:7 replica set'e karşı geçti). + ## [0.14.0] - 2026-11-07 Faz 14: dashboard ana grafiği ve raporlar (14a ve 14b alt fazları). diff --git a/Dockerfile b/Dockerfile index 61b686e..020273f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ # BÜTÇEM — çok aşamalı üretim imajı (Next.js standalone çıktı). # Build: docker build --build-arg APP_COMMIT=$(git rev-parse --short HEAD) -t butcem . # Çalıştır: docker run -p 3000:3000 --env-file .env.production -v butcem-uploads:/app/uploads butcem +# Başlarken: replica set kontrolü, bekleyen migration'lar, ilk admin (docker-entrypoint.sh). ARG NODE_VERSION=26 @@ -22,7 +23,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 \ APP_COMMIT=${APP_COMMIT} COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN npm run build +# Uygulama + başlatma betiği (migration ve ilk admin; tek dosya, tsx gerekmez). +RUN npm run build && npm run build:scripts # 3) Çalışma zamanı — yalnızca standalone çıktı, root olmayan kullanıcı. FROM node:${NODE_VERSION}-alpine AS runner @@ -43,12 +45,15 @@ RUN addgroup -S -g 1001 nodejs && adduser -S -u 1001 -G nodejs nextjs \ COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/dist/bootstrap.mjs ./scripts/bootstrap.mjs +COPY --chown=nextjs:nodejs --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh USER nextjs VOLUME ["/app/uploads"] EXPOSE 3000 -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ +# start-period: ilk deploy'da migration'lar için pay. +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" -CMD ["node", "server.js"] +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 6859b22..fb36333 100644 --- a/README.md +++ b/README.md @@ -52,19 +52,20 @@ Giriş kullanıcı adı **veya** e-posta ile yapılır. Kayıt kapalıdır; kull ## Scriptler -| Komut | Açıklama | -| ----------------------------- | --------------------------------- | -| `npm run dev` | Geliştirme sunucusu | -| `npm run build` / `start` | Üretim build'i / sunucusu | -| `npm run lint` | ESLint | -| `npm run typecheck` | `tsc --noEmit` | -| `npm run test` | Vitest birim testleri | -| `npm run format` | Prettier | -| `npm run db:up` / `db:down` | Lokal MongoDB'yi başlat / durdur | -| `npm run seed` / `seed:reset` | Örnek veri (Faz 4) | -| `npm run test:integration` | Entegrasyon testleri (MongoDB) | -| `npm run migrate` | Bekleyen migration'lar | -| `npm run cleanup` | Eski/bağlanmamış ek dosyaları sil | +| Komut | Açıklama | +| ----------------------------- | -------------------------------------------------------------------------------- | +| `npm run dev` | Geliştirme sunucusu | +| `npm run build` / `start` | Üretim build'i / sunucusu | +| `npm run lint` | ESLint | +| `npm run typecheck` | `tsc --noEmit` | +| `npm run test` | Vitest birim testleri | +| `npm run format` | Prettier | +| `npm run db:up` / `db:down` | Lokal MongoDB'yi başlat / durdur | +| `npm run seed` / `seed:reset` | Örnek veri (Faz 4) | +| `npm run test:integration` | Entegrasyon testleri (MongoDB) | +| `npm run migrate` | Bekleyen migration'lar | +| `npm run bootstrap` | Canlı başlatma adımı (replica set kontrolü + migration; production'da ilk admin) | +| `npm run cleanup` | Eski/bağlanmamış ek dosyaları sil | ## Haneler ve yetki @@ -83,4 +84,13 @@ docker build --build-arg APP_VERSION=$(node -p "require('./package.json').versio --build-arg APP_COMMIT=$(git rev-parse --short HEAD) -t butcem . ``` -Canlı ortam (Dokploy, `butcem.ta1sph.website`) kurulumu Faz 15'te `docs/DEPLOY.md` içinde anlatılacak. +Konteyner başlarken `docker-entrypoint.sh` önce başlatma adımını (`scripts/bootstrap.mjs`: replica +set kontrolü, bekleyen migration'lar, `SEED_ADMIN_*` tanımlıysa ilk admin) sonra uygulamayı çalıştırır. + +## Canlı ortam + +- Kurulum, ortam değişkenleri, domain, volume, otomatik deploy, yedekleme ve geri yükleme: + [`docs/DEPLOY.md`](docs/DEPLOY.md) +- MongoDB replica set ve uygulama kullanıcısı: [`docs/MONGODB-KURULUM.md`](docs/MONGODB-KURULUM.md) +- Duman testi: `scripts/smoke-test.sh https://butcem.ta1sph.website` +- Yedek / geri yükleme betikleri: `scripts/backup/` diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..962f93e --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# BÜTÇEM konteyner girişi: önce başlatma (replica set kontrolü, migration'lar, ilk admin), +# başarılıysa uygulama. Başlatma başarısız olursa konteyner durur; Dokploy günlüğünde sebep görünür. +# Acil durumda atlamak için: SKIP_BOOTSTRAP=1 +set -e +if [ "${SKIP_BOOTSTRAP:-0}" != "1" ]; then + node /app/scripts/bootstrap.mjs +fi +exec node /app/server.js diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..9dce0f3 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,254 @@ +# Canlıya alma (Dokploy + Gitea) + +BÜTÇEM'i Atlas sunucusunda Dokploy ile `https://butcem.ta1sph.website` adresinde yayınlamak, +güncellemek, yedeklemek ve geri yüklemek için adım adım kılavuz. + +**Mimari:** Gitea (`main` dalı) → webhook → Dokploy, Dockerfile ile imaj build eder → konteyner +başlarken veritabanını hazırlar (replica set kontrolü, migration'lar, ilk admin) → Next.js +uygulaması 3000 portunda → Traefik HTTPS ile dışarı açar. MongoDB aynı Dokploy ağında, dışa portsuz. +Yüklenen dosyalar `/app/uploads` volume'unda. + +> Sunucu komutları Atlas'ta çalıştırılır. Her komutun altında ne yaptığı yazıyor. `<…>` ile +> gösterilen yerleri kendi değerlerinizle değiştirin. + +--- + +## 1. Ön koşullar + +- [ ] MongoDB servisi **replica set** olarak çalışıyor ve `butcem_app` kullanıcısı hazır → + [`MONGODB-KURULUM.md`](MONGODB-KURULUM.md) (1–5. adımlar). +- [ ] Gitea'da BÜTÇEM reposu (`main` dalı güncel; `git push origin main`). +- [ ] DNS: `butcem.ta1sph.website` için Atlas'ın genel IP'sine **A kaydı** (ve gerekiyorsa + modemde 80/443 yönlendirmesi Traefik'e). + +```bash +dig +short butcem.ta1sph.website +``` + +Alan adının çözüldüğü IP'yi gösterir; Atlas'ın genel IP'si olmalı. + +## 2. Dokploy'da uygulama + +Dokploy › proje › **Create Service › Application**, ad: `butcem`. + +### 2.1 Kaynak (General) + +- **Provider:** Gitea (Dokploy › Settings › Git'te Gitea bağlantısı tanımlıysa) ya da **Git** + (repo URL'si + Dokploy'un SSH anahtarı Gitea'da deploy key olarak ekli). +- **Repository / Branch:** BÜTÇEM reposu, `main`. +- **Build Type:** `Dockerfile` (yol: `Dockerfile`, context: `.`). +- **Build Args** (isteğe bağlı): `APP_COMMIT=` — sağlık kontrolünde görünür; boşsa + `unknown` yazar. Sürüm `package.json`'dan otomatik gelir. + +### 2.2 Ortam değişkenleri (Environment) + +| Değişken | Değer | Not | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `MONGODB_URI` | `mongodb://butcem_app:@:27017/butcem?authSource=&replicaSet=rs0&directConnection=true` | Ayrıntı: MONGODB-KURULUM.md › 5 | +| `BETTER_AUTH_SECRET` | 32+ karakter rastgele | Aşağıdaki komutla üretin; **değiştirirseniz tüm oturumlar kapanır** | +| `BETTER_AUTH_URL` | `https://butcem.ta1sph.website` | Sonunda `/` yok | +| `SEED_ADMIN_EMAIL` | yönetici e-postası | İlk açılışta admin oluşturulur | +| `SEED_ADMIN_PASSWORD` | güçlü parola (8+) | İlk girişten sonra parolayı değiştirip bu değişkeni silebilirsiniz | +| `MAX_UPLOAD_MB` | `10` | İsteğe bağlı | + +`NODE_ENV=production`, `PORT=3000` ve `UPLOAD_DIR=/app/uploads` imajda tanımlıdır; eklemeyin. +`SEED_USER_PASSWORD` canlıda kullanılmaz (örnek kullanıcı oluşturulmaz). + +```bash +openssl rand -base64 32 +``` + +`BETTER_AUTH_SECRET` için 32 baytlık rastgele bir değer üretir. + +### 2.3 Volume (Advanced › Volumes / Mounts) + +- **Volume Mount** · ad: `butcem-uploads` · bağlama yolu: **`/app/uploads`**. + Fiş, ürün görseli ve ek dosyalar burada tutulur; imaj yeniden oluşturulduğunda kaybolmaz. + (Bind mount yerine adlandırılmış volume kullanın; imajdaki dizin izinleri — uid 1001 — volume'a + ilk oluşturulurken aktarılır.) + +### 2.4 Domain (Domains) + +- **Host:** `butcem.ta1sph.website` · **Path:** `/` · **Container Port:** `3000` +- **HTTPS:** açık · **Certificate:** Let's Encrypt + +### 2.5 İlk deploy + +**Deploy**'a basın ve **Logs**'ta şu satırları bekleyin: + +``` +• MongoDB rs0 PRIMARY +✓ Migration: 001-initial-indexes — … (ilk kurulumda 15 migration) +✓ Admin oluşturuldu: (kullanıcı adı: admin) +✓ Ready in … +``` + +"MongoDB replica set olarak çalışmıyor" hatası görürseniz MONGODB-KURULUM.md › 3'ü uygulayın. +Konteynerin Docker sağlık durumu birkaç saniye içinde **healthy** olur (`/api/health`). + +### 2.6 İlk giriş + +`https://butcem.ta1sph.website` › kullanıcı adı **`admin`** ve `SEED_ADMIN_PASSWORD`. + +1. **Yönetim › Kullanıcılar**'dan aile üyelerini oluşturun. +2. **Yönetim › Haneler**'den haneyi açıp üyeleri ve rollerini ekleyin. +3. (Önerilir) Admin parolasını **Yönetim › Kullanıcılar › kendi satırınız › Şifre sıfırla** ile + değiştirip Dokploy'da `SEED_ADMIN_PASSWORD`'u silin ve yeniden deploy edin; başlatma artık + yalnızca migration'ları çalıştırır. (Admin hesabı varken bu değişken parolayı değiştirmez.) + +## 3. Duman testi (smoke test) + +Kendi bilgisayarınızda, repo klasöründe: + +```bash +scripts/smoke-test.sh https://butcem.ta1sph.website 1.0.0 +``` + +Sağlık (veritabanı, replica set, sürüm), güvenlik başlıkları, HTTP→HTTPS yönlendirmesi ve +oturumsuz erişimin engellendiğini kontrol eder; **veri değiştirmez**. Tümü ✓ olmalı. + +```bash +SMOKE_USER=admin SMOKE_PASSWORD='' scripts/smoke-test.sh https://butcem.ta1sph.website 1.0.0 +``` + +Ayrıca giriş yapıp ana sayfanın açıldığını dener (parola komut geçmişine düşer; sonra +`history -d` ile silebilir ya da satırı boşlukla başlatabilirsiniz). + +Elle kontrol listesi (bir kez, tarayıcıdan): + +- [ ] Giriş / çıkış, telefondan PWA olarak ana ekrana ekleme. +- [ ] Bir hesap ve bir gider işlemi oluşturma (transaction'lı yazma). +- [ ] İşleme fiş fotoğrafı ekleyip görüntüleme (uploads volume). +- [ ] Bir transfer (iki hesap birden; transaction). +- [ ] Dokploy'da uygulamayı **Restart** edip fişin hâlâ açıldığını görme (volume kalıcılığı). + +## 4. Otomatik deploy (Gitea webhook) + +Dokploy › `butcem` › **Deployments** › **Webhook URL**'yi kopyalayın (Auto Deploy açık olmalı). +Gitea › repo › **Settings › Webhooks › Add Webhook › Gitea**: + +- **Target URL:** kopyaladığınız Dokploy URL'si · **HTTP Method:** POST · **Content type:** JSON +- **Trigger On:** Push Events · **Branch filter:** `main` +- Kaydedip **Test Delivery** ile deneyin; Dokploy'da yeni bir deployment başlamalı. + +Bundan sonra `git push origin main` → otomatik build ve yayın. Her deploy'da bekleyen +migration'lar uygulama açılmadan önce çalışır. + +## 5. Güncelleme ve geri alma + +- **Normal güncelleme:** `main`'e push (webhook) ya da Dokploy'da **Deploy**. +- **Büyük güncellemeden önce** elle yedek alın: `sudo /opt/butcem/butcem-backup.sh` (bkz. 6). +- **Geri alma:** Dokploy › Deployments › önceki başarılı deployment › **Rollback** (ya da + Gitea'da önceki commit'e dönüp push). Migration'lar yalnızca ileri yönlüdür; yeni sürümün + eklediği index/alanlar eski sürümü bozmaz. Veri bozulduysa 7'deki geri yüklemeyi kullanın. +- **Acil durum:** başlatma adımı (migration) hata verip uygulama açılmıyorsa geçici olarak + `SKIP_BOOTSTRAP=1` ortam değişkeniyle açabilirsiniz; sorunu giderince kaldırın. + +## 6. Yedekleme + +Günlük: `butcem` veritabanı (`mongodump`) + uploads volume arşivi + SHA-256 özetleri, +**7 gün saklama**, `/var/backups/butcem` altında. Betik: `scripts/backup/butcem-backup.sh`. + +> ⚠️ Şimdilik yedekler **yalnızca sunucuda**. Disk arızasında yedekler de gider; bir sonraki adım +> olarak sunucu dışına kopya (NAS'a `rsync` ya da S3 uyumlu depolama) eklenmeli. + +### 6.1 Kurulum (bir kez) + +```bash +sudo install -d -m 755 /opt/butcem +``` + +Betikler için `/opt/butcem` klasörünü oluşturur. + +```bash +sudo install -m 750 scripts/backup/butcem-backup.sh scripts/backup/butcem-restore.sh /opt/butcem/ +``` + +Yedek ve geri yükleme betiklerini (repo klasöründen; sunucuya `scp` ile de kopyalayabilirsiniz) +`/opt/butcem`'e yalnızca root'un çalıştırabileceği izinle kopyalar. + +```bash +sudo install -m 600 scripts/backup/butcem-backup.env.example /etc/butcem-backup.env +sudo nano /etc/butcem-backup.env +``` + +Ayar dosyasını yalnızca root'un okuyabileceği izinle oluşturur ve düzenlemek için açar. Doldurun: + +- `MONGO_CONTAINER`: MongoDB konteyner adının ayırt edici kısmı (`docker ps --format '{{.Names}}'`). +- `MONGO_ROOT_USER` / `MONGO_ROOT_PASSWORD`: `butcem_root` bilgileri. +- `UPLOADS_VOLUME`: uygulamanın volume adı. + ```bash + docker volume ls | grep -i uploads + ``` + Volume adlarını listeler (Dokploy'da verdiğiniz `butcem-uploads` adını doğrulayın). + +### 6.2 İlk çalıştırma + +```bash +sudo /opt/butcem/butcem-backup.sh +``` + +Yedeği hemen alır; sonunda `Tamam: … veritabanı, … dosya` yazmalı. + +```bash +sudo ls -lh /var/backups/butcem && sudo sh -c 'cd /var/backups/butcem && sha256sum -c SHA256SUMS-*' +``` + +Yedek dosyalarını listeler ve bütünlüklerini doğrular (`OK`). + +### 6.3 Günlük zamanlama + +```bash +sudo crontab -e +``` + +root'un zamanlanmış görevlerini açar. Şu satırı ekleyin (her gece 03:30): + +``` +30 3 * * * /opt/butcem/butcem-backup.sh >> /var/log/butcem-backup.log 2>&1 +``` + +```bash +sudo tail -n 20 /var/log/butcem-backup.log +``` + +Ertesi gün son yedeğin günlüğünü gösterir. + +## 7. Geri yükleme + +```bash +sudo ls -1 /var/backups/butcem +``` + +Mevcut yedekleri listeler; dönülecek tarih damgasını seçin. + +1. Dokploy › `butcem` › **Stop** (yazma olmasın). +2. Geri yükleyin: + ```bash + sudo /opt/butcem/butcem-restore.sh /var/backups/butcem/mongo-.archive.gz /var/backups/butcem/uploads-.tar.gz + ``` + Bütünlüğü kontrol eder, onay ister (`evet`), `butcem` veritabanını yedektekiyle **değiştirir** + ve uploads volume'unu arşivden yeniden yazar. Yalnızca veritabanı için ikinci dosyayı vermeyin. +3. Dokploy › **Start**, ardından duman testi (3). + +**Tatbikat:** Ayda bir, en son yedeği bu adımlarla deneme amaçlı bir Dokploy kopyasına ya da +lokale (`docker compose` mongo'su) yükleyip açılıp açılmadığını kontrol edin. Yedeği hiç denenmemiş +bir yedek, yedek değildir. + +## 8. Sorun giderme + +| Belirti | Neden / çözüm | +| --------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Logs: `MongoDB replica set olarak çalışmıyor` | MONGODB-KURULUM.md › 3 | +| Logs: `Authentication failed` | `MONGODB_URI` kullanıcı/parola/`authSource`; parola URL kodlu mu? | +| Logs: `Geçersiz ortam değişkenleri` | Listelenen değişkeni düzeltin (ör. `BETTER_AUTH_SECRET` < 32 karakter) | +| Konteyner `unhealthy` | `curl -s https://butcem.ta1sph.website/api/health` → `db` alanı; MongoDB servisi çalışıyor mu? | +| Giriş sonrası tekrar giriş sayfası | `BETTER_AUTH_URL` alan adıyla birebir aynı mı (https, sonda `/` yok)? | +| Yüklenen dosya açılmıyor / yükleme hatası | Volume `/app/uploads`'a bağlı mı? `docker exec ls -ln /app/uploads` sahibi 1001 olmalı | +| Sertifika alınamadı | DNS A kaydı ve 80 portu Traefik'e ulaşıyor mu? | + +```bash +docker logs --tail 100 $(docker ps -q --filter name= | head -n1) +``` + +Uygulama konteynerinin son 100 log satırını gösterir (Dokploy › Logs ile aynı). diff --git a/docs/MONGODB-KURULUM.md b/docs/MONGODB-KURULUM.md new file mode 100644 index 0000000..f8df594 --- /dev/null +++ b/docs/MONGODB-KURULUM.md @@ -0,0 +1,144 @@ +# MongoDB kurulumu (canlı, Dokploy) + +BÜTÇEM çok adımlı yazmaları (transfer, "Alışverişi tamamla", kart ödemesi…) **MongoDB transaction** +ile yapar. Transaction yalnızca **replica set** üzerinde çalışır; tek sunucu için **tek düğümlü +replica set** (`rs0`) yeterlidir. Uygulama başlarken bunu kontrol eder; replica set yoksa açık bir +hata mesajıyla durur. + +Bu belge mevcut kurulumu esas alır: Dokploy'da **`mongo:7`** veritabanı servisi (mongo:8 sunucu +çekirdeğiyle uyumsuz), kimlik doğrulama açık, `butcem_root` (yönetici) ve `butcem_app` (uygulama) +kullanıcıları oluşturulmuş. Uygulama mongo:7.0 ile test edilmiştir (tüm entegrasyon testleri). + +> Komutlar Atlas sunucusunda çalıştırılır. `<…>` ile gösterilen yerleri kendi değerlerinizle +> değiştirin. Parolaları komut geçmişine yazmamak için `mongosh` parola sorduğunda girin. + +--- + +## 1. MongoDB konteynerini bul + +```bash +docker ps --format '{{.Names}}' | grep -i mongo +``` + +Çalışan konteynerlerin adlarından MongoDB'ninkini listeler. Dokploy ada rastgele bir son ek ekler +(ör. `butcem-mongo-a1b2c3.1.xyz`). Aşağıda bu adı `` olarak kullanın; adın ayırt edici +başlangıcı (ör. `butcem-mongo-a1b2c3`) da yeterlidir. + +```bash +MONGO=$(docker ps -q --filter name= | head -n1); echo "$MONGO" +``` + +Konteyner kimliğini bir değişkene alır; sonraki komutlar `$MONGO` ile kısalır. + +## 2. Replica set açık mı? + +```bash +docker exec -it "$MONGO" mongosh -u butcem_root --authenticationDatabase admin \ + --eval 'try { const s = rs.status(); print(s.set, s.members[0].stateStr) } catch (e) { print("REPLICA SET YOK: " + e.codeName) }' +``` + +Yönetici olarak bağlanıp replica set durumunu yazdırır (parolayı sorar). + +- Çıktı **`rs0 PRIMARY`** ise replica set hazır; **4. adıma** geçin. +- Çıktı **`REPLICA SET YOK: NoReplicationEnabled`** ise **3. adımı** uygulayın. + +## 3. Tek düğümlü replica set'i aç + +Dokploy'un MongoDB servisinde bir **"Replica Set"** seçeneği varsa (sürüme göre değişir) onu açıp +servisi yeniden deploy etmeniz ve 3.4'e geçmeniz yeterlidir. Yoksa aşağıdaki elle yöntemi uygulayın. +Kimlik doğrulama açıkken replica set bir **anahtar dosyası (keyfile)** ister. + +### 3.1 Yedek alın + +```bash +docker exec -it "$MONGO" mongodump -u butcem_root --authenticationDatabase admin --gzip --archive=/data/db/oncesi.archive.gz +docker cp "$MONGO":/data/db/oncesi.archive.gz ~/mongo-oncesi.archive.gz && docker exec "$MONGO" rm /data/db/oncesi.archive.gz +``` + +Değişiklikten önce tüm veritabanlarının yedeğini konteyner içinde bir dosyaya alır (parolayı sorar), +sonra ev dizinine kopyalayıp konteynerdeki kopyayı siler. Yeni kurulumda veri yoksa atlanabilir. + +### 3.2 Anahtar dosyasını oluştur + +```bash +docker exec "$MONGO" sh -c 'head -c 756 /dev/urandom | base64 > /data/db/rs-keyfile && chmod 400 /data/db/rs-keyfile && chown 999:999 /data/db/rs-keyfile && ls -l /data/db/rs-keyfile' +``` + +Kalıcı veri volume'u (`/data/db`) içinde rastgele bir anahtar dosyası üretir; yalnızca `mongodb` +kullanıcısı (uid 999) okuyabilir. Çıktıda `-r-------- … mongodb mongodb` görmelisiniz. Dosya +volume'da olduğu için yeniden başlatmalarda korunur. + +### 3.3 Başlatma komutunu değiştir ve yeniden deploy et + +Dokploy › MongoDB servisi › **Advanced** sekmesi › **Command** alanına: + +``` +mongod --replSet rs0 --bind_ip_all --keyFile /data/db/rs-keyfile +``` + +yazıp kaydedin ve servisi **Redeploy** edin. (`MONGO_INITDB_ROOT_*` ortam değişkenleri yerinde +kaldığı için kimlik doğrulama açık kalır.) Konteyner yeniden oluşacağından 1. adımdaki `MONGO` +değişkenini yeniden alın. + +### 3.4 Replica set'i başlat (tek seferlik) + +```bash +docker exec -it "$MONGO" mongosh -u butcem_root --authenticationDatabase admin \ + --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })' +``` + +`rs0` adlı tek üyeli replica set'i başlatır; çıktıda `{ ok: 1 }` görmelisiniz. Uygulama +`directConnection=true` ile bağlandığı için üye adresinin `localhost` olması sorun değildir. + +Birkaç saniye sonra 2. adımdaki kontrolü tekrarlayın: **`rs0 PRIMARY`**. + +## 4. Uygulama kullanıcısını doğrula + +```bash +docker exec -it "$MONGO" mongosh -u butcem_root --authenticationDatabase admin --eval ' + for (const d of ["admin", "butcem"]) { + const u = db.getSiblingDB(d).getUser("butcem_app"); + if (u) print("tanımlı veritabanı:", d, "| roller:", JSON.stringify(u.roles)); + }' +``` + +`butcem_app` kullanıcısının hangi veritabanında tanımlı olduğunu (bağlantı dizesindeki +`authSource`) ve rollerini yazdırır. Beklenen rol: `[{"role":"readWrite","db":"butcem"}]`. + +Rol farklıysa düzeltin: + +```bash +docker exec -it "$MONGO" mongosh -u butcem_root --authenticationDatabase admin \ + --eval 'db.getSiblingDB("").updateUser("butcem_app", { roles: [{ role: "readWrite", db: "butcem" }] })' +``` + +Uygulama kullanıcısına yalnızca `butcem` veritabanında okuma-yazma yetkisi verir (migration'lar +index oluşturmak için bu yetkiyle yetinir). + +## 5. Bağlantı dizesi (Dokploy uygulama ortam değişkeni) + +``` +MONGODB_URI=mongodb://butcem_app:@:27017/butcem?authSource=&replicaSet=rs0&directConnection=true +``` + +- ``: Dokploy › MongoDB servisi › **Internal Host** (ör. `butcem-mongo-a1b2c3`). Uygulama + ve veritabanı aynı Dokploy ağında olduğundan **dışa port açmaya gerek yoktur**; açmayın. +- ``: 4. adımın çıktısındaki veritabanı (`admin` veya `butcem`). +- Parolada `@ : / ? # %` gibi karakterler varsa URL kodlayın: + ```bash + python3 -c 'import urllib.parse,getpass; print(urllib.parse.quote(getpass.getpass(), safe=""))' + ``` + Parolayı sorar ve bağlantı dizesine yazılacak kodlu hâlini basar (geçmişe düşmez). + +## 6. Transaction denemesi (isteğe bağlı) + +```bash +docker exec -it "$MONGO" mongosh "mongodb://butcem_app@localhost:27017/butcem?authSource=&replicaSet=rs0&directConnection=true" --eval ' + const s = db.getMongo().startSession(); const c = s.getDatabase("butcem").getCollection("tx_probe"); + s.startTransaction(); c.insertOne({ a: 1 }); s.commitTransaction(); + print("transaction tamam:", db.getSiblingDB("butcem").getCollection("tx_probe").countDocuments({})); + db.getSiblingDB("butcem").getCollection("tx_probe").drop();' +``` + +Uygulama kullanıcısıyla bağlanıp küçük bir transaction yazar, sonucu basar ve deneme +koleksiyonunu siler. `transaction tamam: 1` görmelisiniz. diff --git a/docs/PROMPT.md b/docs/PROMPT.md index 8927ced..e12154b 100644 --- a/docs/PROMPT.md +++ b/docs/PROMPT.md @@ -326,6 +326,7 @@ Aşağıdakileri varsayılan olarak kabul ettim. Faz 0'da yalnızca bunlardan em - **Faz 12 kararları (2026-11-06):** 12a mağazalar ve ürünler (marka/paket, görseller), 12b fiyat kayıtları, birim fiyat ve fiyat geçmişi; `v0.12.0` 12b sonunda. Mağazalar, ürünler ve fiyatlar hane bazlıdır (hane içinde ortak, başka hane görmez). Paket birimi ürün tipinin birimine kendiliğinden çevrilemiyorsa ürün kaydında dönüşüm zorunludur ("5 kg = 33 yıkama"); her fiyat ürün tipinin biriminde karşılaştırılır. - **Faz 13 kararları (2026-11-06):** 13a alışveriş listeleri, stok eşiği önerileri (onayla/reddet), akıllı mağaza önerisi ve alışveriş modu; 13b alışveriş sonrası giriş, karşılaştırma raporu ve "Alışverişi tamamla"; `v0.13.0` 13b sonunda. Birden çok mağazadan alışverişte mağaza başına ayrı bölünmüş gider (her mağaza için ayrı hesap ve fiş). Tamamlanınca liste kapanır ve rapor sabitlenir; alınmayanlar başka aktif listeye taşınır, yeni listeye alınır ya da bırakılır. Reddedilen öneri yeni alım girilene kadar gösterilmez. Markası belirtilmeyen ürün tipi kalemi (ör. pazardan domates) tipin biriminde stoğa işlenir, fiyat kaydı oluşmaz. - **Faz 14 kararları (2026-11-07):** 14a dashboard ana grafiği (Haftalık/Aylık/Altı aylık/Yıllık, kümülatif/dönemsel) ve finans raporları (kategori dağılımı, aylık trend, kişi ve hesap bazlı, bütçe vs. gerçekleşen); 14b cari ve aynî borçlar, birikim kâr/zarar, alışveriş (liste vs. gerçekleşen, mağaza bazlı harcama), birim fiyat geçmişi, fire/zarar; `v0.14.0` 14b sonunda. Raporlar `/raporlar` altında ayrı sayfalar; dönem ve filtreler URL'de, her raporda CSV. "Ay" bütçe ayıdır; gider kuralları bütçeyle aynı (taksit modu, gizli hesap ayarı). Kişi bazlı harcama gideri kaydeden üyeye göre; mağaza bazlı harcama gider alıcısının mağaza adıyla başlamasına göre (şubeler birlikte). +- **Faz 15 kararları (2026-11-07):** Canlı MongoDB Dokploy servisi **mongo:7** (mongo:8 sunucu çekirdeğiyle uyumsuz); uygulama mongo:7.0'a karşı tüm entegrasyon testleriyle doğrulandı. Replica set kimlik doğrulamalı tek düğüm (`rs0`, keyfile), uygulama `directConnection=true` ile bağlanır (`docs/MONGODB-KURULUM.md`). Migration'lar ve ilk admin konteyner başlarken otomatik (`scripts/bootstrap.mjs`; replica set yoksa uygulama açılmaz). Gitea `main` push → Dokploy webhook ile otomatik deploy. Yedek: günlük `mongodump` + uploads arşivi, 7 gün, şimdilik yalnızca sunucuda (sunucu dışı kopya sonraki adım). - **Lokal MongoDB portu:** host'ta `27019` (27017 ve 27018 geliştirme makinesinde dolu). `BUTCEM_MONGO_PORT` ile değiştirilebilir. --- diff --git a/eslint.config.mjs b/eslint.config.mjs index a301bdb..0b49407 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,6 +20,8 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + // Derlenmiş başlatma betiği (esbuild paketi). + "dist/**", "next-env.d.ts", ]), ]); diff --git a/package-lock.json b/package-lock.json index 9418a6f..bd4cf0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "butcem", - "version": "0.14.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "butcem", - "version": "0.14.0", + "version": "1.0.0", "dependencies": { "@hookform/resolvers": "^7.0.2", "@tanstack/react-table": "^9.3.1", diff --git a/package.json b/package.json index c06e0d2..8b75199 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "butcem", - "version": "0.14.0", + "version": "1.0.0", "private": true, "description": "BÜTÇEM — aile bütçe, cari, birikim, ev stoğu ve akıllı alışveriş uygulaması", "engines": { @@ -23,6 +23,8 @@ "icons": "tsx scripts/generate-icons.ts", "test:integration": "vitest run --config vitest.integration.config.mts", "migrate": "tsx --conditions=react-server --env-file=.env.local scripts/migrate.ts", + "bootstrap": "tsx --conditions=react-server --env-file=.env.local scripts/bootstrap.ts", + "build:scripts": "esbuild scripts/bootstrap.ts --bundle --platform=node --format=esm --target=node22 --conditions=react-server --outfile=dist/bootstrap.mjs --external:sharp --external:kerberos --external:snappy --external:socks --external:aws4 --external:@mongodb-js/zstd --external:mongodb-client-encryption --external:gcp-metadata --external:@aws-sdk/credential-providers --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --log-level=warning", "cleanup": "tsx --conditions=react-server --env-file=.env.local scripts/cleanup.ts" }, "dependencies": { diff --git a/scripts/backup/butcem-backup.env.example b/scripts/backup/butcem-backup.env.example new file mode 100644 index 0000000..8f23148 --- /dev/null +++ b/scripts/backup/butcem-backup.env.example @@ -0,0 +1,13 @@ +# /etc/butcem-backup.env — butcem-backup.sh ayarları. İzin: sudo chmod 600 /etc/butcem-backup.env +# Değerler için docs/DEPLOY.md › Yedekleme. + +# MongoDB konteyner adı veya adının ayırt edici bir parçası (docker ps --format '{{.Names}}') +MONGO_CONTAINER="butcem-mongo" +MONGO_ROOT_USER="butcem_root" +MONGO_ROOT_PASSWORD="" + +# Uygulamanın /app/uploads volume adı (docker volume ls) +UPLOADS_VOLUME="butcem-uploads" + +BACKUP_DIR="/var/backups/butcem" +RETENTION_DAYS="7" diff --git a/scripts/backup/butcem-backup.sh b/scripts/backup/butcem-backup.sh new file mode 100755 index 0000000..08f1bc8 --- /dev/null +++ b/scripts/backup/butcem-backup.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# BÜTÇEM günlük yedek — sunucuda (Atlas) root olarak, cron ile çalışır. Ayrıntı: docs/DEPLOY.md › Yedekleme +# +# Üretir (BACKUP_DIR altında, tarih damgalı): +# mongo-YYYYmmdd-HHMMSS.archive.gz → butcem veritabanı (mongodump --archive --gzip) +# uploads-YYYYmmdd-HHMMSS.tar.gz → yüklenen dosyalar (uploads volume) +# SHA256SUMS-YYYYmmdd-HHMMSS → bütünlük özetleri +# RETENTION_DAYS günden eski yedekleri siler. +# +# Ayarlar /etc/butcem-backup.env dosyasından okunur (yalnızca root okuyabilmeli: chmod 600). +set -euo pipefail + +CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}" +# shellcheck source=/dev/null +[ -f "$CONFIG" ] && . "$CONFIG" + +: "${MONGO_CONTAINER:?MONGO_CONTAINER tanımlı değil (MongoDB konteyner adı veya adının bir parçası)}" +: "${MONGO_ROOT_USER:?MONGO_ROOT_USER tanımlı değil}" +: "${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD tanımlı değil}" +: "${UPLOADS_VOLUME:?UPLOADS_VOLUME tanımlı değil (uygulamanın /app/uploads volume adı)}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/butcem}" +RETENTION_DAYS="${RETENTION_DAYS:-7}" +DB_NAME="${DB_NAME:-butcem}" +HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}" + +TS="$(date +%Y%m%d-%H%M%S)" +log() { echo "[$(date '+%F %T')] $*"; } + +# Dokploy konteyner adlarına rastgele son ek ekler; adın bir parçasıyla eşleşen çalışan konteyner. +CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)" +[ -n "$CID" ] || { log "HATA: '${MONGO_CONTAINER}' adlı çalışan MongoDB konteyneri yok."; exit 1; } +docker volume inspect "$UPLOADS_VOLUME" >/dev/null 2>&1 || { log "HATA: '${UPLOADS_VOLUME}' volume'u yok."; exit 1; } + +install -d -m 700 "$BACKUP_DIR" +MONGO_FILE="$BACKUP_DIR/mongo-$TS.archive.gz" +UPLOADS_FILE="$BACKUP_DIR/uploads-$TS.tar.gz" + +# 1) MongoDB: parola komut satırında görünmesin diye geçici yapılandırma dosyasıyla. +log "MongoDB yedeği alınıyor ($DB_NAME)…" +CONF="/tmp/butcem-dump-$TS.yaml" +printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF" +if ! docker exec "$CID" mongodump --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \ + --authenticationDatabase admin --db "$DB_NAME" --archive --gzip >"$MONGO_FILE"; then + docker exec "$CID" rm -f "$CONF" + rm -f "$MONGO_FILE" + log "HATA: mongodump başarısız." + exit 1 +fi +docker exec "$CID" rm -f "$CONF" +[ -s "$MONGO_FILE" ] || { log "HATA: MongoDB yedeği boş."; exit 1; } + +# 2) Yüklenen dosyalar: volume salt okunur bağlanır. +log "Uploads yedeği alınıyor ($UPLOADS_VOLUME)…" +docker run --rm -v "$UPLOADS_VOLUME":/data:ro -v "$BACKUP_DIR":/backup "$HELPER_IMAGE" \ + tar czf "/backup/$(basename "$UPLOADS_FILE")" -C /data . +[ -s "$UPLOADS_FILE" ] || { log "HATA: uploads yedeği boş."; exit 1; } + +# 3) Bütünlük özeti ve izinler +( cd "$BACKUP_DIR" && sha256sum "$(basename "$MONGO_FILE")" "$(basename "$UPLOADS_FILE")" >"SHA256SUMS-$TS" ) +chmod 600 "$MONGO_FILE" "$UPLOADS_FILE" "$BACKUP_DIR/SHA256SUMS-$TS" + +# 4) Saklama süresi +find "$BACKUP_DIR" -maxdepth 1 -type f \( -name 'mongo-*.archive.gz' -o -name 'uploads-*.tar.gz' -o -name 'SHA256SUMS-*' \) \ + -mtime +"$RETENTION_DAYS" -print -delete | sed 's/^/ silindi: /' + +log "Tamam: $(du -h "$MONGO_FILE" | cut -f1) veritabanı, $(du -h "$UPLOADS_FILE" | cut -f1) dosya → $BACKUP_DIR" diff --git a/scripts/backup/butcem-restore.sh b/scripts/backup/butcem-restore.sh new file mode 100755 index 0000000..ee37ccb --- /dev/null +++ b/scripts/backup/butcem-restore.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# BÜTÇEM geri yükleme — sunucuda root olarak, ELLE çalıştırılır. Ayrıntı: docs/DEPLOY.md › Geri yükleme +# +# sudo scripts/backup/butcem-restore.sh /var/backups/butcem/mongo-….archive.gz [/var/backups/butcem/uploads-….tar.gz] +# +# DİKKAT: butcem veritabanındaki koleksiyonlar yedektekilerle DEĞİŞTİRİLİR (--drop); uploads arşivi +# verilirse volume içeriği silinip arşivden yazılır. Önce uygulamayı durdurun (Dokploy › Stop). +# Ayarlar butcem-backup.sh ile aynı dosyadan: /etc/butcem-backup.env +set -euo pipefail + +CONFIG="${BUTCEM_BACKUP_CONFIG:-/etc/butcem-backup.env}" +# shellcheck source=/dev/null +[ -f "$CONFIG" ] && . "$CONFIG" +: "${MONGO_CONTAINER:?}" "${MONGO_ROOT_USER:?}" "${MONGO_ROOT_PASSWORD:?}" "${UPLOADS_VOLUME:?}" +DB_NAME="${DB_NAME:-butcem}" +HELPER_IMAGE="${HELPER_IMAGE:-alpine:3.20}" + +MONGO_FILE="${1:?Kullanım: butcem-restore.sh [uploads-arşivi]}" +UPLOADS_FILE="${2:-}" +[ -s "$MONGO_FILE" ] || { echo "Arşiv yok veya boş: $MONGO_FILE"; exit 1; } +[ -z "$UPLOADS_FILE" ] || [ -s "$UPLOADS_FILE" ] || { echo "Arşiv yok veya boş: $UPLOADS_FILE"; exit 1; } + +# Bütünlük kontrolü (aynı klasörde SHA256SUMS varsa) +DIR="$(cd "$(dirname "$MONGO_FILE")" && pwd)" +STAMP="$(basename "$MONGO_FILE" | sed -E 's/^mongo-(.*)\.archive\.gz$/\1/')" +if [ -f "$DIR/SHA256SUMS-$STAMP" ]; then + (cd "$DIR" && sha256sum -c "SHA256SUMS-$STAMP") || { echo "Bütünlük kontrolü başarısız."; exit 1; } +fi + +CID="$(docker ps -q --filter "name=${MONGO_CONTAINER}" | head -n1)" +[ -n "$CID" ] || { echo "Çalışan MongoDB konteyneri yok: $MONGO_CONTAINER"; exit 1; } + +echo "Geri yüklenecek: $MONGO_FILE${UPLOADS_FILE:+ ve $UPLOADS_FILE}" +echo "'$DB_NAME' veritabanındaki mevcut veri yedektekiyle DEĞİŞTİRİLECEK." +if [ "${ASSUME_YES:-0}" != "1" ]; then + read -r -p "Devam etmek için 'evet' yazın: " ANSWER + [ "$ANSWER" = "evet" ] || { echo "Vazgeçildi."; exit 1; } +fi + +CONF="/tmp/butcem-restore-$$.yaml" +printf 'password: "%s"\n' "$MONGO_ROOT_PASSWORD" | docker exec -i "$CID" sh -c "umask 077 && cat > $CONF" +trap 'docker exec "$CID" rm -f "$CONF" >/dev/null 2>&1 || true' EXIT +docker exec -i "$CID" mongorestore --quiet --config "$CONF" --username "$MONGO_ROOT_USER" \ + --authenticationDatabase admin --archive --gzip --drop --nsInclude "${DB_NAME}.*" <"$MONGO_FILE" +echo "✓ Veritabanı geri yüklendi." + +if [ -n "$UPLOADS_FILE" ]; then + docker run --rm -v "$UPLOADS_VOLUME":/data -v "$(cd "$(dirname "$UPLOADS_FILE")" && pwd)":/backup:ro "$HELPER_IMAGE" \ + sh -c "find /data -mindepth 1 -delete && tar xzf /backup/$(basename "$UPLOADS_FILE") -C /data && chown -R 1001:1001 /data" + echo "✓ Yüklenen dosyalar geri yüklendi." +fi +echo "Uygulamayı yeniden başlatın (Dokploy › Start) ve duman testini çalıştırın." diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts new file mode 100644 index 0000000..3311dfd --- /dev/null +++ b/scripts/bootstrap.ts @@ -0,0 +1,43 @@ +/** + * Canlı başlatma (Docker entrypoint, uygulamadan önce): replica set kontrolü, bekleyen migration'lar + * ve SEED_ADMIN_* tanımlıysa ilk admin hesabı (varsa dokunulmaz). İdempotent; her deploy'da çalışır. + * İmajda esbuild ile tek dosyaya derlenir (scripts/bootstrap.mjs). Lokal: npm run bootstrap + */ +import mongoose from "mongoose"; +import { getDb, getMongoClient } from "@/lib/db/mongo"; +import { env } from "@/lib/env"; +import { runMigrations } from "./migrations"; +import { closeSeedConnections, runSeed } from "./seed/run"; + +async function main() { + const e = env(); + const hello = await getDb().admin().command({ hello: 1 }); + if (!hello.setName) { + throw new Error( + "MongoDB replica set olarak çalışmıyor; transaction'lı yazmalar başarısız olur. " + + "docs/DEPLOY.md › 'MongoDB replica set' adımlarını uygulayın.", + ); + } + if (!hello.isWritablePrimary) throw new Error("MongoDB düğümü PRIMARY değil (yazılamaz)."); + console.log(`• MongoDB ${hello.setName} PRIMARY`); + + if (e.NODE_ENV === "production" && e.SEED_ADMIN_EMAIL && e.SEED_ADMIN_PASSWORD) { + // Production'da seed yalnızca migration + admin yapar (örnek veri oluşturulmaz). + await runSeed(); + } else { + await runMigrations(); + if (e.NODE_ENV === "production") + console.log("• SEED_ADMIN_* tanımlı değil; admin kontrolü atlandı."); + } +} + +main() + .catch((err: unknown) => { + console.error(`✗ Başlatma hatası: ${err instanceof Error ? err.message : err}`); + process.exitCode = 1; + }) + .finally(async () => { + await closeSeedConnections(); + await mongoose.disconnect(); + await getMongoClient().close(); + }); diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..b71b318 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# BÜTÇEM duman testi (smoke test) — canlıya alma sonrası, yalnızca OKUMA yapar; veri değiştirmez. +# +# Kullanım: +# scripts/smoke-test.sh https://butcem.ta1sph.website [beklenen-sürüm] +# SMOKE_USER=admin SMOKE_PASSWORD='…' scripts/smoke-test.sh https://butcem.ta1sph.website 1.0.0 +# +# Gerekenler: bash, curl. Çıkış kodu: 0 = hepsi geçti, 1 = en az bir kontrol başarısız. +set -uo pipefail + +BASE="${1:?Kullanım: scripts/smoke-test.sh [beklenen-sürüm]}" +BASE="${BASE%/}" +EXPECT_VERSION="${2:-}" +FAIL=0 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +bad() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; } +check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; } + +echo "BÜTÇEM duman testi: $BASE" + +echo "1) Sağlık kontrolü" +HEALTH_CODE=$(curl -s -o "$TMP/health" -w '%{http_code}' "$BASE/api/health") +HEALTH=$(cat "$TMP/health") +echo " $HEALTH" +check "GET /api/health 200" '[ "$HEALTH_CODE" = 200 ]' +check "veritabanı erişilebilir" 'grep -q "\"db\":\"ok\"" <<<"$HEALTH"' +check "MongoDB replica set (transaction için)" '! grep -q "\"replicaSet\":null" <<<"$HEALTH" && grep -q "\"replicaSet\":\"" <<<"$HEALTH"' +if [ -n "$EXPECT_VERSION" ]; then + check "sürüm $EXPECT_VERSION" 'grep -q "\"version\":\"$EXPECT_VERSION\"" <<<"$HEALTH"' +fi + +echo "2) Sayfalar ve güvenlik başlıkları" +LOGIN_CODE=$(curl -s -D "$TMP/login-headers" -o /dev/null -w '%{http_code}' "$BASE/login") +H=$(tr -d '\r' <"$TMP/login-headers" | tr 'A-Z' 'a-z') +check "GET /login 200" '[ "$LOGIN_CODE" = 200 ]' +check "Content-Security-Policy var" 'grep -q "^content-security-policy:" <<<"$H"' +check "X-Frame-Options: DENY" 'grep -q "^x-frame-options: deny" <<<"$H"' +check "X-Content-Type-Options: nosniff" 'grep -q "^x-content-type-options: nosniff" <<<"$H"' +if [[ "$BASE" == https://* ]]; then + check "Strict-Transport-Security var" 'grep -q "^strict-transport-security:" <<<"$H"' + HTTP_URL="http://${BASE#https://}/login" + HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HTTP_URL") + check "HTTP → HTTPS yönlendirmesi ($HTTP_CODE)" '[[ "$HTTP_CODE" =~ ^30[1278]$ ]]' +fi + +echo "3) Oturumsuz erişim engelleniyor" +DASH=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$BASE/dashboard") +check "/dashboard → giriş sayfasına yönlendirme ($DASH)" '[[ "$DASH" =~ ^30[27]\ .*/login ]]' +FILE_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/files/000000000000000000000000") +check "/api/files oturumsuz 401 ($FILE_CODE)" '[ "$FILE_CODE" = 401 ]' +CSV=$(curl -s -o "$TMP/csv" -w '%{http_code}' "$BASE/raporlar/kategoriler/csv") +check "rapor CSV'si oturumsuz veri vermiyor ($CSV)" '! grep -q "Toplam gidere oran" "$TMP/csv"' + +if [ -n "${SMOKE_USER:-}" ] && [ -n "${SMOKE_PASSWORD:-}" ]; then + echo "4) Giriş ($SMOKE_USER)" + SIGN=$(curl -s -c "$TMP/jar" -o "$TMP/signin" -w '%{http_code}' -X POST "$BASE/api/auth/sign-in/username" \ + -H 'Content-Type: application/json' -H "Origin: $BASE" \ + --data "$(printf '{"username":"%s","password":"%s"}' "$SMOKE_USER" "$SMOKE_PASSWORD")") + check "giriş 200 ($SIGN)" '[ "$SIGN" = 200 ]' + PAGE=$(curl -s -b "$TMP/jar" -L -o /dev/null -w '%{http_code} %{url_effective}' "$BASE/dashboard") + check "oturumla ana sayfa açılıyor ($PAGE)" '[[ "$PAGE" =~ ^200\ ]] && [[ ! "$PAGE" =~ /login ]]' + curl -s -b "$TMP/jar" -X POST "$BASE/api/auth/sign-out" -H "Origin: $BASE" -o /dev/null +else + echo "4) Giriş testi atlandı (SMOKE_USER ve SMOKE_PASSWORD tanımlı değil)" +fi + +echo +if [ "$FAIL" = 0 ]; then echo "Sonuç: tüm kontroller geçti."; else echo "Sonuç: başarısız kontrol var."; fi +exit "$FAIL" diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 47b26c1..9e826a2 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,5 +1,5 @@ import { connection } from "next/server"; -import { pingDb } from "@/lib/db/mongo"; +import { getDb, pingDb } from "@/lib/db/mongo"; import { APP_COMMIT, APP_VERSION } from "@/lib/version"; /** @@ -10,10 +10,21 @@ export async function GET() { // Cache Components açıkken GET handler'ı build'de prerender edilmesin. await connection(); const dbOk = await pingDb(); + // Transaction'lar için replica set şart; adı yoksa (standalone) null. + let replicaSet: string | null = null; + if (dbOk) { + try { + const hello = await getDb().admin().command({ hello: 1 }, { timeoutMS: 2_000 }); + replicaSet = typeof hello.setName === "string" ? hello.setName : null; + } catch { + replicaSet = null; + } + } return Response.json( { status: dbOk ? "ok" : "degraded", db: dbOk ? "ok" : "unreachable", + replicaSet, version: APP_VERSION, commit: APP_COMMIT, time: new Date().toISOString(), diff --git a/src/components/layout/nav-config.ts b/src/components/layout/nav-config.ts index 88ecae5..c8c1d5e 100644 --- a/src/components/layout/nav-config.ts +++ b/src/components/layout/nav-config.ts @@ -28,7 +28,7 @@ export type NavItem = { /** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */ /** Alt fazlar ondalıkla: 6.1 = Faz 6a. */ -export const CURRENT_PHASE = 14.2; +export const CURRENT_PHASE = 15; export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE;