FRE-4471: Scaffold DarkWatch MVP — monorepo, schema, services, API routes, tests
- Turborepo monorepo structure (packages: api, db, types, jobs; services: darkwatch) - Prisma schema: User, WatchListItem, Exposure, Alert, ScanJob models - WatchListService: CRUD with normalization, dedup, tier-based limits - HIBPService: API integration with severity scoring - MatchingEngine: exact-match with content hash dedup - AlertPipeline: dedup window, email notifications - ScanService: orchestrates watch list -> HIBP -> match -> alert flow - BullMQ job workers for scan and alert processing - Fastify API routes: watchlist, exposures, alerts, scan - Docker Compose: PostgreSQL 16 + Redis 7 - 15 unit tests passing - Implementation plan document uploaded
This commit is contained in:
152
packages/db/prisma/migrations/20260429132230_init/migration.sql
Normal file
152
packages/db/prisma/migrations/20260429132230_init/migration.sql
Normal file
@@ -0,0 +1,152 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionTier" AS ENUM ('BASIC', 'PLUS', 'PREMIUM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IdentifierType" AS ENUM ('EMAIL', 'PHONE', 'SSN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WatchListStatus" AS ENUM ('ACTIVE', 'PAUSED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Severity" AS ENUM ('INFO', 'WARNING', 'CRITICAL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AlertChannel" AS ENUM ('EMAIL', 'PUSH', 'SMS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AlertStatus" AS ENUM ('PENDING', 'SENT', 'READ');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScanJobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DataSource" AS ENUM ('HIBP', 'SECURITY_TRAILS', 'CENSYS', 'SHODAN', 'HONEYPOT');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"subscriptionTier" "SubscriptionTier" NOT NULL DEFAULT 'BASIC',
|
||||
"familyGroupId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WatchListItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"identifierType" "IdentifierType" NOT NULL,
|
||||
"identifierValue" TEXT NOT NULL,
|
||||
"identifierHash" TEXT NOT NULL,
|
||||
"status" "WatchListStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WatchListItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Exposure" (
|
||||
"id" TEXT NOT NULL,
|
||||
"watchListItemId" TEXT NOT NULL,
|
||||
"dataSource" "DataSource" NOT NULL,
|
||||
"breachName" TEXT NOT NULL,
|
||||
"exposedAt" TIMESTAMP(3) NOT NULL,
|
||||
"dataType" TEXT[],
|
||||
"severity" "Severity" NOT NULL,
|
||||
"details" TEXT,
|
||||
"contentHash" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Exposure_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Alert" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"exposureId" TEXT NOT NULL,
|
||||
"severity" "Severity" NOT NULL,
|
||||
"channel" "AlertChannel" NOT NULL,
|
||||
"status" "AlertStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"dedupKey" TEXT NOT NULL,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Alert_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ScanJob" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "ScanJobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"source" "DataSource",
|
||||
"resultCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"errorMessage" TEXT,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ScanJob_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WatchListItem_identifierHash_key" ON "WatchListItem"("identifierHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WatchListItem_userId_idx" ON "WatchListItem"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "WatchListItem_identifierHash_idx" ON "WatchListItem"("identifierHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Exposure_contentHash_key" ON "Exposure"("contentHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Exposure_watchListItemId_idx" ON "Exposure"("watchListItemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Exposure_contentHash_idx" ON "Exposure"("contentHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Exposure_dataSource_idx" ON "Exposure"("dataSource");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Alert_exposureId_key" ON "Alert"("exposureId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_userId_status_idx" ON "Alert"("userId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_dedupKey_idx" ON "Alert"("dedupKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ScanJob_userId_status_idx" ON "ScanJob"("userId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ScanJob_createdAt_idx" ON "ScanJob"("createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WatchListItem" ADD CONSTRAINT "WatchListItem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Exposure" ADD CONSTRAINT "Exposure_watchListItemId_fkey" FOREIGN KEY ("watchListItemId") REFERENCES "WatchListItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_exposureId_fkey" FOREIGN KEY ("exposureId") REFERENCES "Exposure"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ScanJob" ADD CONSTRAINT "ScanJob_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
3
packages/db/prisma/migrations/migration_lock.toml
Normal file
3
packages/db/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
140
packages/db/prisma/schema.prisma
Normal file
140
packages/db/prisma/schema.prisma
Normal file
@@ -0,0 +1,140 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum SubscriptionTier {
|
||||
BASIC
|
||||
PLUS
|
||||
PREMIUM
|
||||
}
|
||||
|
||||
enum IdentifierType {
|
||||
EMAIL
|
||||
PHONE
|
||||
SSN
|
||||
}
|
||||
|
||||
enum WatchListStatus {
|
||||
ACTIVE
|
||||
PAUSED
|
||||
}
|
||||
|
||||
enum Severity {
|
||||
INFO
|
||||
WARNING
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
enum AlertChannel {
|
||||
EMAIL
|
||||
PUSH
|
||||
SMS
|
||||
}
|
||||
|
||||
enum AlertStatus {
|
||||
PENDING
|
||||
SENT
|
||||
READ
|
||||
}
|
||||
|
||||
enum ScanJobStatus {
|
||||
PENDING
|
||||
RUNNING
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum DataSource {
|
||||
HIBP
|
||||
SECURITY_TRAILS
|
||||
CENSYS
|
||||
SHODAN
|
||||
HONEYPOT
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
name String?
|
||||
subscriptionTier SubscriptionTier @default(BASIC)
|
||||
familyGroupId String?
|
||||
watchListItems WatchListItem[]
|
||||
alerts Alert[]
|
||||
scanJobs ScanJob[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model WatchListItem {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
identifierType IdentifierType
|
||||
identifierValue String
|
||||
identifierHash String @unique
|
||||
status WatchListStatus @default(ACTIVE)
|
||||
exposures Exposure[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([identifierHash])
|
||||
}
|
||||
|
||||
model Exposure {
|
||||
id String @id @default(uuid())
|
||||
watchListItemId String
|
||||
watchListItem WatchListItem @relation(fields: [watchListItemId], references: [id], onDelete: Cascade)
|
||||
dataSource DataSource
|
||||
breachName String
|
||||
exposedAt DateTime
|
||||
dataType String[]
|
||||
severity Severity
|
||||
details String?
|
||||
contentHash String @unique
|
||||
alert Alert?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([watchListItemId])
|
||||
@@index([contentHash])
|
||||
@@index([dataSource])
|
||||
}
|
||||
|
||||
model Alert {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
exposureId String @unique
|
||||
exposure Exposure @relation(fields: [exposureId], references: [id], onDelete: Cascade)
|
||||
severity Severity
|
||||
channel AlertChannel
|
||||
status AlertStatus @default(PENDING)
|
||||
dedupKey String
|
||||
sentAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId, status])
|
||||
@@index([dedupKey])
|
||||
}
|
||||
|
||||
model ScanJob {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
status ScanJobStatus @default(PENDING)
|
||||
source DataSource?
|
||||
resultCount Int @default(0)
|
||||
errorMessage String?
|
||||
completedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId, status])
|
||||
@@index([createdAt])
|
||||
}
|
||||
24
packages/db/prisma/seed.ts
Normal file
24
packages/db/prisma/seed.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import prisma from "../src";
|
||||
|
||||
async function main() {
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: "dev@shieldai.local" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "dev@shieldai.local",
|
||||
name: "Dev User",
|
||||
subscriptionTier: "PREMIUM",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Seeded user:", user.email);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user