Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ MYSQL_DATABASE=prfc_portal
MYSQL_USER=prfc_user
MYSQL_PASSWORD=prfc_password

# SMTP Configuration
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@example.com
SMTP_PASS=your-password
# Email (Resend HTTP API)
# RESEND_API_KEY=re_your_api_key_here
FROM_EMAIL=noreply@example.com

# Token Authentication (shared secret with PRFC portal)
Expand Down
5 changes: 0 additions & 5 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
DATABASE_URL="mysql://test:test@localhost:3306/test"
SMTP_HOST="smtp.test.com"
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER="test@test.com"
SMTP_PASS="testpass"
FROM_EMAIL="noreply@test.com"
NODE_ENV="test"
UNSUBSCRIBE_SECRET="test-secret-key-must-be-at-least-32-chars"
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ jobs:
env:
CI: "true"
DATABASE_URL: "mysql://root:root@localhost:3306/prfc_ci"
SMTP_HOST: "smtp.test.com"
SMTP_USER: "test"
SMTP_PASS: "test"
FROM_EMAIL: "test@test.com"

steps:
Expand Down
95 changes: 74 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
"lucide-react": "^0.555.0",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"nodemailer": "^7.0.11",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"resend": "^6.9.4",
"server-only": "^0.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
Expand All @@ -89,7 +89,6 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/twilio": "^3.19.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
-- AlterTable referral: widen PII columns for encryption, drop member_email index
ALTER TABLE `referral` MODIFY COLUMN `member_name` VARCHAR(512) NOT NULL;
ALTER TABLE `referral` MODIFY COLUMN `member_email` VARCHAR(512) NOT NULL;
ALTER TABLE `referral` MODIFY COLUMN `prospect_name` VARCHAR(512) NOT NULL;
ALTER TABLE `referral` MODIFY COLUMN `prospect_email` VARCHAR(512) NOT NULL;
DROP INDEX `referral_member_email_idx` ON `referral`;

-- CreateTable contact_group
CREATE TABLE `contact_group` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`description` VARCHAR(500) NULL,
`ownerid` INTEGER NOT NULL,

INDEX `contact_group_ownerid_idx`(`ownerid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable contact_group_member
CREATE TABLE `contact_group_member` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`group_id` INTEGER NOT NULL,
`member_id` INTEGER NOT NULL,
`notify_email` BOOLEAN NOT NULL DEFAULT true,
`notify_sms` BOOLEAN NOT NULL DEFAULT false,
`added_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`added_by` INTEGER NOT NULL,
`unsubscribed_at` DATETIME(3) NULL,
`unsubscribe_method` VARCHAR(50) NULL,

UNIQUE INDEX `contact_group_member_group_id_member_id_key`(`group_id`, `member_id`),
INDEX `contact_group_member_group_id_notify_email_idx`(`group_id`, `notify_email`),
INDEX `contact_group_member_group_id_notify_sms_idx`(`group_id`, `notify_sms`),
INDEX `contact_group_member_member_id_idx`(`member_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable sms_consent
CREATE TABLE `sms_consent` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`member_id` INTEGER NOT NULL,
`phone` VARCHAR(512) NOT NULL,
`phone_hash` VARCHAR(64) NOT NULL,
`consented_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`consent_method` VARCHAR(50) NOT NULL,
`consent_text` TEXT NOT NULL,
`consent_purpose` VARCHAR(100) NOT NULL,
`ip_address` VARCHAR(512) NULL,
`revoked_at` DATETIME(3) NULL,
`revoke_method` VARCHAR(50) NULL,
`revoke_message` VARCHAR(500) NULL,

INDEX `sms_consent_member_id_idx`(`member_id`),
INDEX `sms_consent_phone_hash_idx`(`phone_hash`),
INDEX `sms_consent_consented_at_idx`(`consented_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable email_suppression
CREATE TABLE `email_suppression` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`email` VARCHAR(512) NOT NULL,
`email_hash` VARCHAR(64) NOT NULL,
`reason` ENUM('hard_bounce', 'complaint', 'unsubscribe') NOT NULL,
`suppressed_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),

UNIQUE INDEX `email_suppression_email_hash_key`(`email_hash`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable message
CREATE TABLE `message` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`group_id` INTEGER NULL,
`sender_id` INTEGER NOT NULL,
`subject` VARCHAR(200) NOT NULL,
`body` TEXT NOT NULL,
`sent_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`email_count` INTEGER NOT NULL DEFAULT 0,
`sms_count` INTEGER NOT NULL DEFAULT 0,
`failed_count` INTEGER NOT NULL DEFAULT 0,
`is_blast` BOOLEAN NOT NULL DEFAULT false,

INDEX `message_group_id_idx`(`group_id`),
INDEX `message_sender_id_idx`(`sender_id`),
INDEX `message_sent_at_idx`(`sent_at`),
INDEX `message_group_id_sent_at_idx`(`group_id`, `sent_at`),
INDEX `message_is_blast_sent_at_idx`(`is_blast`, `sent_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable message_recipient
CREATE TABLE `message_recipient` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`message_id` INTEGER NOT NULL,
`member_id` INTEGER NOT NULL,
`channel` VARCHAR(10) NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'pending',
`external_id` VARCHAR(100) NULL,
`sent_at` DATETIME(3) NULL,
`delivered_at` DATETIME(3) NULL,
`error` VARCHAR(500) NULL,

UNIQUE INDEX `message_recipient_message_id_member_id_channel_key`(`message_id`, `member_id`, `channel`),
INDEX `message_recipient_message_id_idx`(`message_id`),
INDEX `message_recipient_member_id_idx`(`member_id`),
INDEX `message_recipient_message_id_status_idx`(`message_id`, `status`),
INDEX `message_recipient_member_id_sent_at_idx`(`member_id`, `sent_at`),
INDEX `message_recipient_external_id_idx`(`external_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey contact_group_member -> contact_group
ALTER TABLE `contact_group_member` ADD CONSTRAINT `contact_group_member_group_id_fkey` FOREIGN KEY (`group_id`) REFERENCES `contact_group`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey message -> contact_group
ALTER TABLE `message` ADD CONSTRAINT `message_group_id_fkey` FOREIGN KEY (`group_id`) REFERENCES `contact_group`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey message_recipient -> message
ALTER TABLE `message_recipient` ADD CONSTRAINT `message_recipient_message_id_fkey` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
2 changes: 2 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ function assertSafeToSeed() {
process.exit(1);
}

if (process.env.ALLOW_REMOTE_SEED === "true") return;

const dbUrl = process.env.DATABASE_URL ?? "";
if (/prod|production|live/i.test(dbUrl)) {
console.error("DATABASE_URL appears to reference production");
Expand Down
9 changes: 1 addition & 8 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.url(),

SMTP_HOST: z.string().min(1).optional(),
SMTP_PORT: z.coerce.number().default(587),
SMTP_SECURE: z
.string()
.default("false")
.transform((v) => v === "true"),
SMTP_USER: z.string().min(1).optional(),
SMTP_PASS: z.string().min(1).optional(),
RESEND_API_KEY: z.string().min(1).optional(),
FROM_EMAIL: z.email().optional(),

UPSTASH_REDIS_REST_URL: z.url().optional(),
Expand Down
Loading
Loading