2b648975ae
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
6.7 KiB
TypeScript
202 lines
6.7 KiB
TypeScript
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<typeof getAuth>["api"];
|
||
|
||
async function findUserIdByUsername(username: string): Promise<string | null> {
|
||
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<Headers> {
|
||
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<SeedSummary> {
|
||
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<string, string>([[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<string, string[]>();
|
||
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();
|
||
}
|