diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee6aceb..ca6f304 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: jsdom: specifier: ^29.1.1 version: 29.1.1 + tsx: + specifier: ^4.22.3 + version: 4.22.3 vite-plugin-solid: specifier: ^2.11.12 version: 2.11.12(solid-js@1.9.13)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..1baeffc --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/shieldai" diff --git a/web/package.json b/web/package.json index cead89c..55ab564 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,11 @@ "start": "vite start", "preview": "vite preview", "test": "vitest run", - "lint": "tsc --noEmit" + "lint": "tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "tsx src/server/db/migrate.ts", + "db:seed": "tsx src/server/db/seed.ts" }, "dependencies": { "@solidjs/meta": "^0.29.4", @@ -34,6 +38,7 @@ "@types/pg": "^8.20.0", "drizzle-kit": "^0.31.10", "jsdom": "^29.1.1", + "tsx": "^4.22.3", "vite-plugin-solid": "^2.11.12", "vitest": "^4.1.5" } diff --git a/web/src/server/db/db.test.ts b/web/src/server/db/db.test.ts new file mode 100644 index 0000000..0cca27b --- /dev/null +++ b/web/src/server/db/db.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; + +describe("db module", () => { + it("exports db and pool", async () => { + const mod = await import("./index"); + expect(mod.db).toBeDefined(); + expect(mod.pool).toBeDefined(); + }); +}); + +describe("migrate module", () => { + it("exports runMigrations function", async () => { + const mod = await import("./migrate"); + expect(mod.runMigrations).toBeInstanceOf(Function); + }); +}); + +describe("seed module", () => { + it("exports seed function", async () => { + const mod = await import("./seed"); + expect(mod.seed).toBeInstanceOf(Function); + }); +}); diff --git a/web/src/server/db/index.ts b/web/src/server/db/index.ts index 6750c09..70e31b1 100644 --- a/web/src/server/db/index.ts +++ b/web/src/server/db/index.ts @@ -5,6 +5,18 @@ import * as schema from "./schema"; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/shieldai", + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, }); export const db = drizzle(pool, { schema }); +export { pool }; + +process.on("SIGTERM", () => { + pool.end().catch(() => process.exit(1)); +}); + +process.on("SIGINT", () => { + pool.end().catch(() => process.exit(1)); +}); diff --git a/web/src/server/db/migrate.ts b/web/src/server/db/migrate.ts new file mode 100644 index 0000000..6f1fd5f --- /dev/null +++ b/web/src/server/db/migrate.ts @@ -0,0 +1,23 @@ +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { db, pool } from "./index"; + +export async function runMigrations() { + console.log("[db] Running migrations..."); + try { + await migrate(db, { migrationsFolder: "./drizzle" }); + console.log("[db] Migrations completed successfully"); + } catch (error) { + console.error("[db] Migration failed:", error); + throw error; + } +} + +const isMainModule = process.argv[1]?.includes("migrate"); +if (isMainModule) { + runMigrations() + .then(() => pool.end()) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/web/src/server/db/seed.ts b/web/src/server/db/seed.ts new file mode 100644 index 0000000..6daa6a4 --- /dev/null +++ b/web/src/server/db/seed.ts @@ -0,0 +1,308 @@ +import { db, pool } from "./index"; +import { + users, + familyGroups, + familyGroupMembers, + subscriptions, + watchlistItems, + exposures, + alerts, + blogPosts, + propertyWatchlistItems, + infoBrokers, + removalRequests, +} from "./schema"; + +const ALICE_ID = "a0000000-0000-0000-0000-000000000001"; +const BOB_ID = "a0000000-0000-0000-0000-000000000002"; +const CAROL_ID = "a0000000-0000-0000-0000-000000000003"; +const FAMILY_GROUP_ID = "a0000000-0000-0000-0000-000000000010"; +const ALICE_SUB_ID = "a0000000-0000-0000-0000-000000000020"; +const BOB_SUB_ID = "a0000000-0000-0000-0000-000000000021"; +const WATCH_EMAIL_ALICE = "a0000000-0000-0000-0000-000000000030"; +const WATCH_PHONE_ALICE = "a0000000-0000-0000-0000-000000000031"; +const WATCH_EMAIL_BOB = "a0000000-0000-0000-0000-000000000032"; +const WATCH_SSN_BOB = "a0000000-0000-0000-0000-000000000033"; +const WATCH_DOMAIN_BOB = "a0000000-0000-0000-0000-000000000034"; +const EXPOSURE_1 = "a0000000-0000-0000-0000-000000000040"; +const EXPOSURE_2 = "a0000000-0000-0000-0000-000000000041"; +const EXPOSURE_3 = "a0000000-0000-0000-0000-000000000042"; +const PROPERTY_1 = "a0000000-0000-0000-0000-000000000050"; +const PROPERTY_2 = "a0000000-0000-0000-0000-000000000051"; +const BROKER_ID = "a0000000-0000-0000-0000-000000000060"; +const REMOVAL_REQUEST_ID = "a0000000-0000-0000-0000-000000000070"; + +export async function seed() { + console.log("[seed] Seeding database..."); + + await db.insert(users).values([ + { id: ALICE_ID, email: "alice@example.com", name: "Alice Smith", role: "family_admin", emailVerified: new Date() }, + { id: BOB_ID, email: "bob@example.com", name: "Bob Smith", role: "user", emailVerified: new Date() }, + { id: CAROL_ID, email: "carol@example.com", name: "Carol Johnson", role: "user" }, + ]).onConflictDoNothing(); + console.log("[seed] Users created"); + + await db.insert(familyGroups).values([ + { id: FAMILY_GROUP_ID, name: "Smith Family", ownerId: ALICE_ID }, + ]).onConflictDoNothing(); + console.log("[seed] Family group created"); + + await db.insert(familyGroupMembers).values([ + { groupId: FAMILY_GROUP_ID, userId: ALICE_ID, role: "owner" }, + { groupId: FAMILY_GROUP_ID, userId: BOB_ID, role: "member" }, + ]).onConflictDoNothing(); + console.log("[seed] Family group members created"); + + const now = new Date(); + const periodStart = new Date(now.getFullYear(), now.getMonth(), 1); + const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); + + await db.insert(subscriptions).values([ + { + id: ALICE_SUB_ID, + userId: ALICE_ID, + familyGroupId: FAMILY_GROUP_ID, + tier: "premium", + status: "active", + currentPeriodStart: periodStart, + currentPeriodEnd: periodEnd, + }, + { + id: BOB_SUB_ID, + userId: BOB_ID, + tier: "basic", + status: "active", + currentPeriodStart: periodStart, + currentPeriodEnd: periodEnd, + }, + ]).onConflictDoNothing(); + console.log("[seed] Subscriptions created"); + + await db.insert(watchlistItems).values([ + { id: WATCH_EMAIL_ALICE, subscriptionId: ALICE_SUB_ID, type: "email", value: "alice@example.com", hash: "hash-alice-email" }, + { id: WATCH_PHONE_ALICE, subscriptionId: ALICE_SUB_ID, type: "phoneNumber", value: "+15551234567", hash: "hash-alice-phone" }, + { id: WATCH_EMAIL_BOB, subscriptionId: BOB_SUB_ID, type: "email", value: "bob@example.com", hash: "hash-bob-email" }, + { id: WATCH_SSN_BOB, subscriptionId: BOB_SUB_ID, type: "ssn", value: "XXX-XX-1234", hash: "hash-bob-ssn" }, + { id: WATCH_DOMAIN_BOB, subscriptionId: BOB_SUB_ID, type: "domain", value: "bobsmith.example.com", hash: "hash-bob-domain" }, + ]).onConflictDoNothing(); + console.log("[seed] Watchlist items created"); + + const pastDate = (daysAgo: number) => new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000); + + await db.insert(exposures).values([ + { + id: EXPOSURE_1, + subscriptionId: ALICE_SUB_ID, + watchlistItemId: WATCH_EMAIL_ALICE, + source: "hibp", + dataType: "email", + identifier: "alice@example.com", + identifierHash: "hash-alice-email", + severity: "critical", + metadata: { breach: "Adobe 2013", dataClasses: ["email", "password_hint", "password"] }, + isFirstTime: true, + detectedAt: pastDate(30), + }, + { + id: EXPOSURE_2, + subscriptionId: ALICE_SUB_ID, + watchlistItemId: WATCH_PHONE_ALICE, + source: "darkWebForum", + dataType: "phoneNumber", + identifier: "+15551234567", + identifierHash: "hash-alice-phone", + severity: "warning", + metadata: { forum: "Nulled.to", postedAt: "2025-04-01" }, + detectedAt: pastDate(14), + }, + { + id: EXPOSURE_3, + subscriptionId: BOB_SUB_ID, + watchlistItemId: WATCH_EMAIL_BOB, + source: "hibp", + dataType: "email", + identifier: "bob@example.com", + identifierHash: "hash-bob-email", + severity: "info", + metadata: { breach: "LinkedIn 2021", dataClasses: ["email", "name"] }, + detectedAt: pastDate(7), + }, + ]).onConflictDoNothing(); + console.log("[seed] Exposures created"); + + await db.insert(alerts).values([ + { + subscriptionId: ALICE_SUB_ID, + userId: ALICE_ID, + exposureId: EXPOSURE_1, + type: "exposure_detected", + title: "Critical breach detected: Adobe 2013", + message: "Your email alice@example.com was found in the Adobe 2013 data breach.", + severity: "critical", + channel: ["email", "push"], + createdAt: pastDate(30), + }, + { + subscriptionId: ALICE_SUB_ID, + userId: ALICE_ID, + exposureId: EXPOSURE_2, + type: "exposure_detected", + title: "Phone number found on dark web forum", + message: "Your phone number was posted on Nulled.to.", + severity: "warning", + channel: ["push"], + createdAt: pastDate(14), + }, + { + subscriptionId: ALICE_SUB_ID, + userId: ALICE_ID, + type: "scan_complete", + title: "Dark web scan completed", + message: "Weekly dark web scan found 2 new exposures.", + severity: "info", + channel: ["email"], + createdAt: pastDate(1), + }, + { + subscriptionId: BOB_SUB_ID, + userId: BOB_ID, + exposureId: EXPOSURE_3, + type: "exposure_detected", + title: "Breach detected: LinkedIn 2021", + message: "Your email bob@example.com was found in the LinkedIn 2021 breach.", + severity: "info", + channel: ["email", "push"], + createdAt: pastDate(7), + }, + { + subscriptionId: BOB_SUB_ID, + userId: BOB_ID, + type: "scan_complete", + title: "Monthly scan complete", + message: "Your monthly dark web scan has finished. 1 exposure found.", + severity: "info", + channel: ["email"], + createdAt: pastDate(0), + }, + { + subscriptionId: ALICE_SUB_ID, + userId: ALICE_ID, + type: "exposure_resolved", + title: "Exposure resolved: Adobe 2013", + message: "The Adobe 2013 breach exposure has been marked as resolved.", + severity: "info", + channel: ["push"], + createdAt: pastDate(0), + }, + ]).onConflictDoNothing(); + console.log("[seed] Alerts created"); + + await db.insert(blogPosts).values([ + { + slug: "what-is-dark-web-monitoring", + title: "What Is Dark Web Monitoring and Why You Need It", + excerpt: "Learn how dark web monitoring protects your personal information from cybercriminals.", + content: "The dark web is a hidden part of the internet where cybercriminals buy and sell stolen data. ShieldAI helps you monitor your digital footprint...", + authorName: "ShieldAI Team", + tags: ["dark-web", "monitoring", "security"], + published: true, + publishedAt: pastDate(60), + viewCount: 1250, + }, + { + slug: "protect-your-family-online", + title: "5 Tips to Protect Your Family's Online Privacy", + excerpt: "Simple steps to keep your family's personal information safe from data brokers.", + content: "In today's digital age, protecting your family's privacy is more important than ever. Here are five actionable tips...", + authorName: "ShieldAI Team", + tags: ["family", "privacy", "tips"], + published: true, + publishedAt: pastDate(30), + viewCount: 820, + }, + { + slug: "understanding-data-brokers", + title: "Understanding Data Brokers: Who Has Your Information?", + excerpt: "A comprehensive guide to data brokers and how to opt out of their databases.", + content: "Data brokers collect and sell personal information. This guide explains how they operate and how you can remove your data...", + authorName: "ShieldAI Team", + tags: ["data-brokers", "privacy", "opt-out"], + published: true, + publishedAt: pastDate(7), + viewCount: 340, + }, + ]).onConflictDoNothing(); + console.log("[seed] Blog posts created"); + + await db.insert(propertyWatchlistItems).values([ + { + id: PROPERTY_1, + subscriptionId: ALICE_SUB_ID, + address: "123 Main St, Springfield, IL 62701", + parcelId: "SPR-1234-5678", + ownerName: "Alice Smith", + streetAddress: "123 Main St", + city: "Springfield", + state: "IL", + zipCode: "62701", + latitude: 39.7817, + longitude: -89.6501, + }, + { + id: PROPERTY_2, + subscriptionId: ALICE_SUB_ID, + address: "456 Oak Ave, Springfield, IL 62704", + parcelId: "SPR-8765-4321", + ownerName: "Alice Smith", + streetAddress: "456 Oak Ave", + city: "Springfield", + state: "IL", + zipCode: "62704", + latitude: 39.7654, + longitude: -89.6321, + }, + ]).onConflictDoNothing(); + console.log("[seed] Property watchlist items created"); + + await db.insert(infoBrokers).values([ + { + id: BROKER_ID, + name: "PeopleFind Pro", + domain: "peoplefindpro.example.com", + category: "PEOPLE_SEARCH", + removalMethod: "MANUAL_FORM", + removalUrl: "https://peoplefindpro.example.com/opt-out", + requiresAccount: true, + requiresVerification: true, + estimatedDays: 30, + }, + ]).onConflictDoNothing(); + console.log("[seed] Info brokers created"); + + await db.insert(removalRequests).values([ + { + id: REMOVAL_REQUEST_ID, + subscriptionId: ALICE_SUB_ID, + brokerId: BROKER_ID, + status: "PENDING", + personalInfo: { name: "Alice Smith", email: "alice@example.com", phone: "+15551234567" }, + method: "MANUAL_FORM", + attempts: 2, + nextRetryAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), + notes: "Previous attempts failed due to CAPTCHA", + }, + ]).onConflictDoNothing(); + console.log("[seed] Removal requests created"); + + console.log("[seed] Seeding complete"); +} + +const isMainModule = process.argv[1]?.includes("seed"); +if (isMainModule) { + seed() + .then(() => pool.end()) + .catch((error) => { + console.error("[seed] Error:", error); + process.exit(1); + }); +}