- 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
153 lines
4.7 KiB
SQL
153 lines
4.7 KiB
SQL
-- 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;
|