From 54d83cc6b9bdac6b1a7430f9f40b231ee18bc94f Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 01:08:10 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20env=20do=C4=9Frulama,=20Vitest,=20Pret?= =?UTF-8?q?tier=20ve=20edit=C3=B6r=20ayarlar=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .editorconfig | 12 +++++++++ .env.example | 22 ++++++++++++++++ .gitignore | 9 ++++++- .nvmrc | 1 + .prettierignore | 7 +++++ .prettierrc | 9 +++++++ .vscode/extensions.json | 10 +++++++ .vscode/settings.json | 14 ++++++++++ scripts/migrations/.gitkeep | 0 scripts/seed.ts | 10 +++++++ src/lib/env.test.ts | 33 +++++++++++++++++++++++ src/lib/env.ts | 52 +++++++++++++++++++++++++++++++++++++ vitest.config.mts | 14 ++++++++++ 13 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 scripts/migrations/.gitkeep create mode 100644 scripts/seed.ts create mode 100644 src/lib/env.test.ts create mode 100644 src/lib/env.ts create mode 100644 vitest.config.mts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b5488b --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# BÜTÇEM ortam değişkenleri — kopyala: cp .env.example .env.local +# Gerçek değerler yalnızca .env.local (lokal) ve Dokploy ortam değişkenlerinde (canlı) tutulur. + +# Lokal: docker compose ile tek düğümlü replica set (transaction için zorunlu) +MONGODB_URI="mongodb://localhost:27019/butcem?replicaSet=rs0&directConnection=true" + +# Üret: openssl rand -base64 32 +BETTER_AUTH_SECRET="" +# Lokal: http://localhost:3000 — Canlı: https://butcem.ta1sph.website +BETTER_AUTH_URL="http://localhost:3000" + +# Faz 4 seed'i için ilk admin hesabı +SEED_ADMIN_EMAIL="admin@butcem.local" +SEED_ADMIN_PASSWORD="" + +# Ek dosyalar (canlıda Docker volume: /app/uploads) +UPLOAD_DIR="./uploads" +MAX_UPLOAD_MB="10" + +# Build sırasında Dockerfile tarafından doldurulur; lokalde boş bırakılabilir +APP_VERSION="" +APP_COMMIT="" diff --git a/.gitignore b/.gitignore index 5ef6a52..56ea362 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files (only the example is committed) .env* +!.env.example # vercel .vercel @@ -39,3 +40,9 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# user uploads (never committed; backed up separately) +/uploads/ + +# local tooling +/scratch/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..6f4247a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +26 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f37c117 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +.next +node_modules +package-lock.json +coverage +uploads +next-env.d.ts +docs/PROMPT.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c2f882b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./src/app/globals.css", + "tailwindFunctions": ["cn", "cva"] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..76fbf0f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "vitest.explorer", + "mongodb.mongodb-vscode", + "editorconfig.editorconfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..103298c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "files.eol": "\n", + "tailwindCSS.experimental.classRegex": [["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]], + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/scripts/migrations/.gitkeep b/scripts/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..9f80154 --- /dev/null +++ b/scripts/seed.ts @@ -0,0 +1,10 @@ +/** + * Seed betiği — Faz 4'te doldurulacak (admin, örnek hane, aile üyeleri ve alan verisi). + * Çalıştırma: npm run seed | Sıfırlayıp doldurma: npm run seed:reset + */ +if (process.env.NODE_ENV === "production" && process.argv.includes("--reset")) { + console.error("seed:reset production ortamında çalıştırılamaz."); + process.exit(1); +} + +console.log("Seed henüz uygulanmadı (Faz 4)."); diff --git a/src/lib/env.test.ts b/src/lib/env.test.ts new file mode 100644 index 0000000..a85c678 --- /dev/null +++ b/src/lib/env.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { parseServerEnv } from "./env"; + +const valid = { + MONGODB_URI: "mongodb://localhost:27019/butcem?replicaSet=rs0&directConnection=true", + BETTER_AUTH_SECRET: "x".repeat(32), + BETTER_AUTH_URL: "http://localhost:3000", +}; + +describe("parseServerEnv", () => { + it("geçerli değerleri kabul eder ve varsayılanları uygular", () => { + const env = parseServerEnv(valid); + expect(env.UPLOAD_DIR).toBe("./uploads"); + expect(env.MAX_UPLOAD_MB).toBe(10); + expect(env.NODE_ENV).toBe("development"); + }); + + it("MAX_UPLOAD_MB string'ini sayıya çevirir", () => { + expect(parseServerEnv({ ...valid, MAX_UPLOAD_MB: "25" }).MAX_UPLOAD_MB).toBe(25); + }); + + it("boş string'i tanımsız sayar", () => { + const env = parseServerEnv({ ...valid, SEED_ADMIN_EMAIL: "", APP_VERSION: "" }); + expect(env.SEED_ADMIN_EMAIL).toBeUndefined(); + expect(env.APP_VERSION).toBeUndefined(); + }); + + it("eksik ve hatalı değişkenleri tek hata mesajında listeler", () => { + expect(() => + parseServerEnv({ MONGODB_URI: "postgres://x", BETTER_AUTH_SECRET: "kisa" }), + ).toThrowError(/MONGODB_URI[\s\S]*BETTER_AUTH_SECRET[\s\S]*BETTER_AUTH_URL/); + }); +}); diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..0ecadac --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +/** + * Sunucu ortam değişkenleri şeması. + * + * Doğrulama tembel (lazy) yapılır: `next build` sırasında (ör. Docker build aşamasında) + * sırlar henüz tanımlı olmadığı için modül yüklenirken değil, ilk `env()` çağrısında çalışır. + * Sunucu ilk isteği işlerken eksik/hatalı bir değişken varsa açık bir hata ile durur. + */ +export const serverEnvSchema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + MONGODB_URI: z + .string() + .min(1, "MONGODB_URI zorunlu") + .refine((v) => v.startsWith("mongodb://") || v.startsWith("mongodb+srv://"), { + message: "MONGODB_URI mongodb:// veya mongodb+srv:// ile başlamalı", + }), + BETTER_AUTH_SECRET: z.string().min(32, "BETTER_AUTH_SECRET en az 32 karakter olmalı"), + BETTER_AUTH_URL: z.url("BETTER_AUTH_URL geçerli bir URL olmalı"), + SEED_ADMIN_EMAIL: z.email().optional(), + SEED_ADMIN_PASSWORD: z.string().min(8).optional(), + UPLOAD_DIR: z.string().min(1).default("./uploads"), + MAX_UPLOAD_MB: z.coerce.number().int().positive().max(100).default(10), + APP_VERSION: z.string().optional(), + APP_COMMIT: z.string().optional(), +}); + +export type ServerEnv = z.infer; + +/** Boş string'leri tanımsız say (`.env` dosyalarında `KEY=""` yaygın). */ +function normalize(source: Record) { + return Object.fromEntries(Object.entries(source).map(([k, v]) => [k, v === "" ? undefined : v])); +} + +export function parseServerEnv(source: Record): ServerEnv { + const result = serverEnvSchema.safeParse(normalize(source)); + if (!result.success) { + const issues = result.error.issues + .map((i) => ` - ${i.path.join(".") || "(kök)"}: ${i.message}`) + .join("\n"); + throw new Error(`Geçersiz ortam değişkenleri:\n${issues}`); + } + return result.data; +} + +let cached: ServerEnv | undefined; + +/** Doğrulanmış sunucu ortam değişkenleri. Yalnızca sunucu kodunda kullan. */ +export function env(): ServerEnv { + cached ??= parseServerEnv(process.env); + return cached; +} diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..c50fe37 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,14 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts", "scripts/**/*.test.ts"], + // "server-only" paketi test ortamında hata fırlatmasın. + server: { deps: { inline: ["server-only"] } }, + }, +});