feat(seed): örnek aile kullanıcıları, haneler ve roller (idempotent)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:14:10 +03:00
parent 38e2a7a26d
commit 2b648975ae
7 changed files with 369 additions and 53 deletions
+201
View File
@@ -0,0 +1,201 @@
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();
}