Initial commit

This commit is contained in:
Developer
2026-02-06 21:44:04 -06:00
commit f85e93c7a6
151 changed files with 22916 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');
-- CreateEnum
CREATE TYPE "RegistrationStatus" AS ENUM ('CONFIRMED', 'PAYMENT_PENDING', 'PAID', 'CANCELLED');
-- CreateEnum
CREATE TYPE "WebinarVisibility" AS ENUM ('PUBLIC', 'PRIVATE');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"image" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"role" "Role" NOT NULL DEFAULT 'USER',
"firstName" TEXT,
"lastName" TEXT,
"gender" TEXT,
"dob" TIMESTAMP(3),
"address" TEXT,
"forcePasswordReset" BOOLEAN NOT NULL DEFAULT false,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL DEFAULT 'oauth',
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refreshToken" TEXT,
"accessToken" TEXT,
"expiresAt" INTEGER,
"tokenType" TEXT,
"scope" TEXT,
"idToken" TEXT,
"sessionState" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Credential" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Credential_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Verification" (
"id" TEXT NOT NULL,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT,
CONSTRAINT "Verification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SystemConfig" (
"id" INTEGER NOT NULL,
"data" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AppSetup" (
"id" INTEGER NOT NULL,
"googleAuthEnabled" BOOLEAN NOT NULL DEFAULT false,
"googleClientId" TEXT,
"googleClientSecret" TEXT,
"socials" JSONB NOT NULL DEFAULT '{}',
"categories" JSONB NOT NULL DEFAULT '[]',
"paginationItemsPerPage" INTEGER NOT NULL DEFAULT 10,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AppSetup_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Webinar" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"speaker" TEXT NOT NULL,
"startAt" TIMESTAMP(3) NOT NULL,
"duration" INTEGER NOT NULL,
"bannerUrl" TEXT,
"category" TEXT NOT NULL,
"visibility" "WebinarVisibility" NOT NULL DEFAULT 'PUBLIC',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"capacity" INTEGER NOT NULL,
"priceCents" INTEGER NOT NULL DEFAULT 0,
"meetingInfo" JSONB NOT NULL DEFAULT '{}',
"learningPoints" JSONB NOT NULL DEFAULT '[]',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Webinar_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WebinarRegistration" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"webinarId" TEXT NOT NULL,
"status" "RegistrationStatus" NOT NULL DEFAULT 'CONFIRMED',
"stripeCheckoutSessionId" TEXT,
"stripePaymentIntentId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WebinarRegistration_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ContactMessage" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"message" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'NEW',
"adminNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"authorId" TEXT,
CONSTRAINT "ContactMessage_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Credential_userId_key" ON "Credential"("userId");
-- CreateIndex
CREATE INDEX "Credential_userId_idx" ON "Credential"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "Verification_userId_idx" ON "Verification"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Verification_identifier_value_key" ON "Verification"("identifier", "value");
-- CreateIndex
CREATE INDEX "Webinar_visibility_idx" ON "Webinar"("visibility");
-- CreateIndex
CREATE INDEX "Webinar_isActive_idx" ON "Webinar"("isActive");
-- CreateIndex
CREATE INDEX "WebinarRegistration_userId_idx" ON "WebinarRegistration"("userId");
-- CreateIndex
CREATE INDEX "WebinarRegistration_webinarId_idx" ON "WebinarRegistration"("webinarId");
-- CreateIndex
CREATE UNIQUE INDEX "WebinarRegistration_userId_webinarId_key" ON "WebinarRegistration"("userId", "webinarId");
-- CreateIndex
CREATE INDEX "ContactMessage_status_idx" ON "ContactMessage"("status");
-- CreateIndex
CREATE INDEX "ContactMessage_createdAt_idx" ON "ContactMessage"("createdAt");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Credential" ADD CONSTRAINT "Credential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Verification" ADD CONSTRAINT "Verification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WebinarRegistration" ADD CONSTRAINT "WebinarRegistration_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WebinarRegistration" ADD CONSTRAINT "WebinarRegistration_webinarId_fkey" FOREIGN KEY ("webinarId") REFERENCES "Webinar"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ContactMessage" ADD CONSTRAINT "ContactMessage_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

202
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,202 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
ADMIN
USER
}
enum RegistrationStatus {
CONFIRMED
PAYMENT_PENDING
PAID
CANCELLED
}
enum WebinarVisibility {
PUBLIC
PRIVATE
}
// BetterAuth User model
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Custom fields
role Role @default(USER)
firstName String?
lastName String?
gender String?
dob DateTime?
address String?
forcePasswordReset Boolean @default(false)
isActive Boolean @default(true)
// BetterAuth relations
accounts Account[]
credential Credential?
sessions Session[]
verifications Verification[]
// Custom relations
registrations WebinarRegistration[]
contactMessages ContactMessage[] @relation("messageAuthor")
}
// BetterAuth Account model (OAuth and email/password accounts)
// Note: For email/password, credentials are stored differently by BetterAuth
model Account {
id String @id @default(cuid())
userId String
type String @default("oauth")
provider String
providerAccountId String
refreshToken String?
accessToken String?
expiresAt Int?
tokenType String?
scope String?
idToken String?
sessionState String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
}
// Credential table for email/password authentication
model Credential {
id String @id @default(cuid())
userId String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
// BetterAuth Session model
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
// BetterAuth Verification model
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String?
@@unique([identifier, value])
@@index([userId])
}
model SystemConfig {
id Int @id
data Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AppSetup {
id Int @id
googleAuthEnabled Boolean @default(false)
googleClientId String?
googleClientSecret String?
socials Json @default("{}")
categories Json @default("[]")
paginationItemsPerPage Int @default(10)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Webinar {
id String @id @default(uuid())
title String
description String
speaker String
startAt DateTime
duration Int
bannerUrl String?
category String
visibility WebinarVisibility @default(PUBLIC)
isActive Boolean @default(true)
capacity Int
priceCents Int @default(0)
meetingInfo Json @default("{}")
learningPoints Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
registrations WebinarRegistration[]
@@index([visibility])
@@index([isActive])
}
model WebinarRegistration {
id String @id @default(uuid())
userId String
webinarId String
status RegistrationStatus @default(CONFIRMED)
stripeCheckoutSessionId String?
stripePaymentIntentId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
webinar Webinar @relation(fields: [webinarId], references: [id])
@@unique([userId, webinarId])
@@index([userId])
@@index([webinarId])
}
model ContactMessage {
id String @id @default(uuid())
name String
email String
subject String
message String
status String @default("NEW") // NEW, READ, REPLIED
adminNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String?
author User? @relation("messageAuthor", fields: [authorId], references: [id])
@@index([status])
@@index([createdAt])
}

171
prisma/seed.mjs Normal file
View File

@@ -0,0 +1,171 @@
import bcrypt from "bcryptjs";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const adminEmail = "admin@ywyw.com";
const customerEmail = "cust@ywyw.com";
const passwordHash = await bcrypt.hash("Dev1234#", 10);
const admin = await prisma.user.upsert({
where: { email: adminEmail },
update: {},
create: {
email: adminEmail,
name: "Admin User",
role: "ADMIN",
firstName: "Admin",
lastName: "User",
gender: "OTHER",
emailVerified: true,
forcePasswordReset: false,
isActive: true,
},
});
await prisma.credential.upsert({
where: { userId: admin.id },
update: { password: passwordHash },
create: { userId: admin.id, password: passwordHash },
});
console.log("✅ Admin user created:", adminEmail);
const customer = await prisma.user.upsert({
where: { email: customerEmail },
update: {},
create: {
email: customerEmail,
name: "Customer User",
role: "USER",
firstName: "Customer",
lastName: "User",
gender: "OTHER",
emailVerified: true,
forcePasswordReset: false,
isActive: true,
},
});
await prisma.credential.upsert({
where: { userId: customer.id },
update: { password: passwordHash },
create: { userId: customer.id, password: passwordHash },
});
console.log("✅ Customer user created:", customerEmail);
const existingWebinars = await prisma.webinar.count();
if (existingWebinars === 0) {
const now = Date.now();
const sample = [
{
title: "Estate Planning Fundamentals",
description: "Learn the foundations of estate planning, wills, and trusts.",
speaker: "Emily Roberts",
startAt: new Date(now + 7 * 86400000),
duration: 90,
bannerUrl: null,
category: "Basics",
visibility: "PUBLIC",
isActive: true,
capacity: 50,
priceCents: 0,
learningPoints: [
"Understanding the basics of wills and trusts",
"Key differences between revocable and irrevocable trusts",
"Common estate planning mistakes to avoid",
"When to update your estate plan",
],
meetingInfo: {},
},
{
title: "Avoiding Probate: Strategies & Solutions",
description: "Practical strategies to reduce or avoid probate delays.",
speaker: "David Martinez",
startAt: new Date(now + 10 * 86400000),
duration: 75,
bannerUrl: null,
category: "Planning",
visibility: "PUBLIC",
isActive: true,
capacity: 60,
priceCents: 0,
learningPoints: [
"How probate works and why it can be costly",
"Living trusts as probate avoidance tools",
"Joint ownership strategies",
"Beneficiary designations and their importance",
],
meetingInfo: {},
},
{
title: "Tax-Efficient Wealth Transfer",
description: "Minimize taxes when transferring assets to heirs.",
speaker: "Susan Chen",
startAt: new Date(now + 14 * 86400000),
duration: 60,
bannerUrl: null,
category: "Advanced",
visibility: "PUBLIC",
isActive: true,
capacity: 40,
priceCents: 0,
learningPoints: [
"Federal and state estate tax basics",
"Gift tax exclusions and lifetime exemptions",
"Charitable giving strategies",
"Generation-skipping transfer tax considerations",
],
meetingInfo: {},
},
];
for (const w of sample) {
await prisma.webinar.create({ data: w });
}
console.log(`✅ Created ${sample.length} sample webinars`);
} else {
console.log(` Skipped webinar seeding - ${existingWebinars} already exist`);
}
const existingRegistrations = await prisma.webinarRegistration.count();
if (existingRegistrations === 0) {
const webinars = await prisma.webinar.findMany({ take: 2 });
if (webinars.length >= 2) {
await prisma.webinarRegistration.create({
data: {
userId: customer.id,
webinarId: webinars[0].id,
status: "CONFIRMED",
},
});
await prisma.webinarRegistration.create({
data: {
userId: customer.id,
webinarId: webinars[1].id,
status: "CONFIRMED",
},
});
console.log("✅ Created sample registrations for customer");
}
} else {
console.log(` Skipped registration seeding - ${existingRegistrations} already exist`);
}
}
main()
.then(async () => {
await prisma.$disconnect();
console.log("✅ Seeding completed successfully");
})
.catch(async (e) => {
console.error("❌ Seeding failed:", e);
await prisma.$disconnect();
process.exit(1);
});

191
prisma/seed.ts Normal file
View File

@@ -0,0 +1,191 @@
import bcrypt from "bcryptjs";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const adminEmail = "admin@ywyw.com";
const customerEmail = "cust@ywyw.com";
const passwordHash = await bcrypt.hash("Dev1234#", 10);
const admin = await prisma.user.upsert({
where: { email: adminEmail },
update: {},
create: {
email: adminEmail,
name: "Admin User",
role: "ADMIN",
firstName: "Admin",
lastName: "User",
gender: "OTHER",
emailVerified: true,
forcePasswordReset: false,
isActive: true,
},
});
await prisma.credential.upsert({
where: { userId: admin.id },
update: { password: passwordHash },
create: { userId: admin.id, password: passwordHash },
});
console.log("✅ Admin user created:", adminEmail);
const customer = await prisma.user.upsert({
where: { email: customerEmail },
update: {},
create: {
email: customerEmail,
name: "Customer User",
role: "USER",
firstName: "Customer",
lastName: "User",
gender: "OTHER",
emailVerified: true,
forcePasswordReset: false,
isActive: true,
},
});
await prisma.credential.upsert({
where: { userId: customer.id },
update: { password: passwordHash },
create: { userId: customer.id, password: passwordHash },
});
console.log("✅ Customer user created:", customerEmail);
const existingWebinars = await prisma.webinar.count();
if (existingWebinars === 0) {
const now = Date.now();
const sample = [
{
title: "Estate Planning Fundamentals",
description: "Learn the foundations of estate planning, wills, and trusts.",
speaker: "Emily Roberts",
startAt: new Date(now + 7 * 86400000),
duration: 90,
bannerUrl: null,
category: "Basics",
visibility: "PUBLIC",
isActive: true,
capacity: 50,
priceCents: 0,
learningPoints: [
"Understanding the basics of wills and trusts",
"Key differences between revocable and irrevocable trusts",
"Common estate planning mistakes to avoid",
"When to update your estate plan",
],
meetingInfo: {},
},
{
title: "Avoiding Probate: Strategies & Solutions",
description: "Practical strategies to reduce or avoid probate delays.",
speaker: "David Martinez",
startAt: new Date(now + 10 * 86400000),
duration: 75,
bannerUrl: null,
category: "Planning",
visibility: "PUBLIC",
isActive: true,
capacity: 60,
priceCents: 0,
learningPoints: [
"How probate works and why it can be costly",
"Living trusts as probate avoidance tools",
"Joint ownership strategies",
"Beneficiary designations and their importance",
],
meetingInfo: {},
},
{
title: "Tax-Efficient Estate Planning",
description: "Minimize taxes and preserve wealth across generations.",
speaker: "Jennifer Thompson",
startAt: new Date(now + 14 * 86400000),
duration: 90,
bannerUrl: null,
category: "Tax",
visibility: "PUBLIC",
isActive: true,
capacity: 40,
priceCents: 4900,
learningPoints: [
"Current federal and state estate tax exemptions",
"Gift tax strategies and annual exclusions",
"Charitable giving techniques for tax benefits",
"Generation-skipping transfer tax planning",
],
meetingInfo: {},
},
{
title: "Healthcare Directives & Powers of Attorney",
description: "Understand advanced directives and medical decision-making.",
speaker: "Lisa Patterson",
startAt: new Date(now + 17 * 86400000),
duration: 60,
bannerUrl: null,
category: "Healthcare",
visibility: "PUBLIC",
isActive: true,
capacity: 80,
priceCents: 0,
learningPoints: [
"Types of healthcare directives and their purposes",
"Choosing the right healthcare proxy",
"Living wills vs. healthcare powers of attorney",
"HIPAA authorizations and medical records access",
],
meetingInfo: {},
},
{
title: "Family Wealth Transfer (Private Session)",
description: "Invite-only workshop for complex family asset structures.",
speaker: "Michael Chen",
startAt: new Date(now + 21 * 86400000),
duration: 120,
bannerUrl: null,
category: "Advanced",
visibility: "PRIVATE",
isActive: true,
capacity: 20,
priceCents: 9900,
learningPoints: [
"Advanced trust structures for wealth preservation",
"Family limited partnerships and LLCs",
"Succession planning for family businesses",
"Coordinating estate plans across multiple jurisdictions",
],
meetingInfo: {},
},
];
await prisma.webinar.createMany({ data: sample as any });
console.log("✅ Sample webinars created");
}
const appSetup = await prisma.appSetup.findUnique({ where: { id: 1 } });
if (!appSetup) {
await prisma.appSetup.create({
data: {
id: 1,
googleAuthEnabled: false,
socials: {},
categories: ["Basics", "Planning", "Tax", "Healthcare", "Advanced"],
},
});
console.log("✅ App setup row created");
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});