import mongoose, { Types } from "mongoose"; import { getAuth } from "@/lib/auth"; import { getDb } from "@/lib/db/mongo"; import { connectMongoose } from "@/lib/db/mongoose"; import { env } from "@/lib/env"; import { HouseholdModel } from "@/models/household"; import { HouseholdMemberModel } from "@/models/household-member"; import { storedEmail } from "@/shared/account-email"; import { ADMIN_USERNAME, SEED_HOUSEHOLDS, SEED_USERS, type SeedUser } from "./fixtures"; /** * İdempotent seed. Kullanıcılar Better Auth API'siyle oluşturulur (şifreler doğru hashlenir); * tekrar çalıştırınca kopya oluşturmaz (kullanıcı adı / hane adı / üyelik çiftine göre). * Production'da yalnızca admin oluşturulur; örnek kullanıcı ve hane oluşturulmaz. */ export type SeedOptions = { reset?: boolean; log?: (line: string) => void }; export type SeedAccountRow = { rol: string; kullaniciAdi: string; eposta: string; durum: string; haneler: string; }; export type SeedSummary = { created: { users: number; households: number; memberships: number }; accounts: SeedAccountRow[]; }; const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"]; const DOMAIN_COLLECTIONS = ["households", "householdMembers"]; type AuthApi = ReturnType["api"]; async function findUserIdByUsername(username: string): Promise { const doc = await getDb() .collection("user") .findOne({ username }, { projection: { _id: 1 } }); return doc ? String(doc._id) : null; } /** Admin olarak giriş yapıp oturum başlıklarını döner (ban gibi oturum isteyen admin API'leri için). */ async function adminHeaders(api: AuthApi, password: string): Promise { const { headers } = await api.signInUsername({ body: { username: ADMIN_USERNAME, password }, returnHeaders: true, }); const cookie = headers .getSetCookie() .map((c) => c.split(";")[0]) .join("; "); return new Headers({ cookie }); } export async function runSeed({ reset = false, log = console.log, }: SeedOptions = {}): Promise { const e = env(); const isProd = e.NODE_ENV === "production"; if (reset && isProd) throw new Error("seed:reset production ortamında çalıştırılamaz."); if (!e.SEED_ADMIN_EMAIL || !e.SEED_ADMIN_PASSWORD) { throw new Error("SEED_ADMIN_EMAIL ve SEED_ADMIN_PASSWORD tanımlı olmalı."); } if (!isProd && !e.SEED_USER_PASSWORD) { throw new Error("SEED_USER_PASSWORD tanımlı olmalı (örnek kullanıcıların ortak şifresi)."); } const api = getAuth().api; const db = getDb(); await connectMongoose(); const summary: SeedSummary = { created: { users: 0, households: 0, memberships: 0 }, accounts: [], }; if (reset) { for (const name of [...AUTH_COLLECTIONS, ...DOMAIN_COLLECTIONS]) { await db.collection(name).deleteMany({}); } log("• Kimlik doğrulama ve hane koleksiyonları temizlendi."); } // Index'ler (production'da Mongoose autoIndex kapalı olduğu için açıkça oluşturulur). await HouseholdModel.createIndexes(); await HouseholdMemberModel.createIndexes(); // 1) Admin const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase(); let adminId = await findUserIdByUsername(ADMIN_USERNAME); if (!adminId) { const res = await api.createUser({ body: { email: adminEmail, password: e.SEED_ADMIN_PASSWORD, name: "Yönetici", role: "admin", data: { username: ADMIN_USERNAME }, }, }); adminId = res.user.id; summary.created.users++; log(`✓ Admin oluşturuldu: ${adminEmail} (kullanıcı adı: ${ADMIN_USERNAME})`); } else { log(`• Admin zaten var: ${ADMIN_USERNAME}`); } if (isProd) { log("• Production: örnek kullanıcı ve hane oluşturulmadı."); return summary; } // 2) Örnek kullanıcılar const ids = new Map([[ADMIN_USERNAME, adminId]]); const toBan: SeedUser[] = []; for (const u of SEED_USERS) { let id = await findUserIdByUsername(u.username); if (!id) { const res = await api.createUser({ body: { email: storedEmail(u.username, u.email), password: e.SEED_USER_PASSWORD!, name: u.name, role: u.role, data: { username: u.username }, }, }); id = res.user.id; summary.created.users++; log(`✓ Kullanıcı: ${u.name} (@${u.username})`); } ids.set(u.username, id); if (u.banReason) toBan.push(u); } // 3) Askıdaki kullanıcı(lar) — admin API'si oturum ister. const pendingBans: { user: SeedUser; id: string }[] = []; for (const u of toBan) { const id = ids.get(u.username)!; const doc = await db .collection("user") .findOne({ _id: new Types.ObjectId(id) }, { projection: { banned: 1 } }); if (!doc?.banned) pendingBans.push({ user: u, id }); } if (pendingBans.length) { const headers = await adminHeaders(api, e.SEED_ADMIN_PASSWORD); for (const { user, id } of pendingBans) { await api.banUser({ headers, body: { userId: id, banReason: user.banReason } }); log(`✓ Askıya alındı: @${user.username}`); } } // 4) Haneler ve üyelikler const memberships = new Map(); const actor = new Types.ObjectId(adminId); for (const h of SEED_HOUSEHOLDS) { let household = await HouseholdModel.findOne({ name: h.name }); if (!household) { household = await HouseholdModel.create({ name: h.name, createdBy: actor, updatedBy: actor }); summary.created.households++; log(`✓ Hane: ${h.name}`); } for (const m of h.members) { const userId = new Types.ObjectId(ids.get(m.username)!); const result = await HouseholdMemberModel.updateOne( { householdId: household._id, userId }, { $setOnInsert: { householdId: household._id, userId, createdBy: actor }, $set: { role: m.role, updatedBy: actor }, }, { upsert: true }, ); if (result.upsertedCount) summary.created.memberships++; memberships.set(m.username, [ ...(memberships.get(m.username) ?? []), `${h.name} (${m.role})`, ]); } } // 5) Geliştirme hesapları tablosu const all = [ { username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const }, ...SEED_USERS, ]; summary.accounts = all.map((u) => ({ rol: u.role, kullaniciAdi: u.username, eposta: u.email ?? "—", durum: "banReason" in u && u.banReason ? "askıda" : "aktif", haneler: (memberships.get(u.username) ?? ["—"]).join(", "), })); return summary; } /** Seed sonrası bağlantıları kapatır (CLI ve testler). */ export async function closeSeedConnections() { await mongoose.disconnect(); }